use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use parking_lot::Mutex;
use truce_core::buffer::AudioBuffer;
use truce_core::bus::BusLayout;
use truce_core::editor::Editor;
use truce_core::events::{EventBody, EventList};
use truce_core::info::PluginInfo;
use truce_core::plugin::PluginRuntime;
use truce_core::process::{ProcessContext, ProcessStatus};
use truce_params::Params;
use truce_params::sample::Sample;
use crate::loader::NativeLoader;
macro_rules! hot_debug {
($($arg:tt)*) => {
#[cfg(feature = "hot-debug")]
eprintln!($($arg)*);
};
}
const GUI_LOCK_WAIT: Duration = Duration::from_millis(50);
pub struct HotShell<P: Params, S: Sample = f32> {
pub params: Arc<P>,
loader: Arc<Mutex<NativeLoader<S>>>,
meters: Arc<[AtomicU32; 256]>,
sample_rate: f64,
max_block_size: usize,
last_seen_load_counter: u64,
latency_cache: AtomicU32,
tail_cache: AtomicU32,
}
unsafe impl<P: Params, S: Sample> Send for HotShell<P, S> {}
impl<P: Params + 'static, S: Sample> HotShell<P, S> {
pub fn new(params: P, dylib_path: PathBuf) -> Self {
let params = Arc::new(params);
let params_ptr = Arc::as_ptr(¶ms).cast::<()>();
let loader = NativeLoader::new(dylib_path, params_ptr);
let initial_counter = loader.load_counter();
let loader = Arc::new(Mutex::new(loader));
NativeLoader::spawn_watcher(&loader);
Self {
params,
loader,
meters: Arc::new(std::array::from_fn(|_| AtomicU32::new(0))),
sample_rate: 44100.0,
max_block_size: 1024,
last_seen_load_counter: initial_counter,
latency_cache: AtomicU32::new(0),
tail_cache: AtomicU32::new(0),
}
}
#[must_use]
pub fn try_editor(&self) -> Option<Box<dyn Editor>> {
let loader = self.loader.try_lock_for(GUI_LOCK_WAIT)?;
let plugin = loader.plugin()?;
Some(plugin.editor())
}
}
impl<P: Params + 'static, S: Sample> PluginRuntime for HotShell<P, S> {
type Sample = S;
fn info() -> PluginInfo
where
Self: Sized,
{
unreachable!("HotShell::info() should not be called statically")
}
fn bus_layouts() -> Vec<BusLayout>
where
Self: Sized,
{
unreachable!("HotShell::bus_layouts() should not be called statically")
}
fn init(&mut self) {}
fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
self.sample_rate = sample_rate;
self.max_block_size = max_block_size;
self.params.set_sample_rate(sample_rate);
let Some(mut loader) = self.loader.try_lock() else {
return;
};
if let Some(plugin) = loader.plugin_mut() {
plugin.reset(sample_rate, max_block_size);
self.latency_cache
.store(plugin.latency(), Ordering::Relaxed);
self.tail_cache.store(plugin.tail(), Ordering::Relaxed);
}
}
fn process(
&mut self,
buffer: &mut AudioBuffer<S>,
events: &EventList,
context: &mut ProcessContext,
) -> ProcessStatus {
let Some(mut loader) = self.loader.try_lock() else {
return ProcessStatus::Normal;
};
let counter = loader.load_counter();
if counter != self.last_seen_load_counter {
if let Some(plugin) = loader.plugin_mut() {
plugin.reset(self.sample_rate, self.max_block_size);
}
self.last_seen_load_counter = counter;
}
let Some(plugin) = loader.plugin_mut() else {
return ProcessStatus::Normal;
};
for e in events.iter() {
if let EventBody::ParamChange { id, value } = &e.body {
self.params.set_plain(*id, *value);
}
}
self.params.snap_smoothers();
let params = &self.params;
let meters = &self.meters;
let param_fn = |id: u32| -> f64 { params.get_plain(id).unwrap_or(0.0) };
let meter_fn = |id: u32, v: f32| {
let idx = id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
if let Some(slot) = meters.get(idx) {
slot.store(v.to_bits(), Ordering::Relaxed);
}
};
let mut ctx = ProcessContext::new(
context.transport,
context.sample_rate,
buffer.num_samples(),
&mut *context.output_events,
)
.with_params(¶m_fn)
.with_meters(&meter_fn);
let status = plugin.process(buffer, events, &mut ctx);
self.latency_cache
.store(plugin.latency(), Ordering::Relaxed);
self.tail_cache.store(plugin.tail(), Ordering::Relaxed);
status
}
fn save_state(&self) -> Vec<u8> {
let Some(loader) = self.loader.try_lock_for(GUI_LOCK_WAIT) else {
return Vec::new();
};
loader
.plugin()
.map(truce_plugin::PluginLogicCore::save_state)
.unwrap_or_default()
}
fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
let Some(mut loader) = self.loader.try_lock_for(GUI_LOCK_WAIT) else {
return Ok(());
};
let Some(plugin) = loader.plugin_mut() else {
return Ok(());
};
let result = plugin.load_state(data);
plugin.state_changed();
result
}
fn editor(&mut self) -> Option<Box<dyn Editor>> {
hot_debug!("[truce-hot] editor() called");
self.try_editor()
}
fn latency(&self) -> u32 {
self.latency_cache.load(Ordering::Relaxed)
}
fn tail(&self) -> u32 {
self.tail_cache.load(Ordering::Relaxed)
}
fn get_meter(&self, meter_id: u32) -> f32 {
let idx = meter_id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
self.meters
.get(idx)
.map_or(0.0, |v| f32::from_bits(v.load(Ordering::Relaxed)))
}
}