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::config::{AudioConfig, ProcessMode};
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, StateOrigin};
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>>>,
state: *mut (),
state_origin: Option<StateOrigin>,
meters: Arc<truce_core::meters::MeterStore>,
snapshots: Arc<truce_core::snapshot::SnapshotSlot>,
try_snapshot: bool,
last_snapshot_version: Option<u64>,
sample_rate: f64,
max_block_size: usize,
process_mode: ProcessMode,
state_generation: u64,
reset_pending: bool,
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_swap = loader.swap_generation();
let (state, state_origin) = loader
.init_state()
.map_or((std::ptr::null_mut(), None), |(st, origin)| {
(st, Some(origin))
});
let loader = Arc::new(Mutex::new(loader));
NativeLoader::spawn_watcher(&loader);
Self {
params,
loader,
state,
state_origin,
meters: truce_core::meters::MeterStore::new(),
snapshots: truce_core::snapshot::SnapshotSlot::new(),
try_snapshot: true,
last_snapshot_version: None,
sample_rate: 44100.0,
max_block_size: 1024,
process_mode: ProcessMode::Realtime,
state_generation: initial_swap,
reset_pending: false,
latency_cache: AtomicU32::new(0),
tail_cache: AtomicU32::new(0),
}
}
fn ensure_state(&mut self, loader: &NativeLoader<S>) -> bool {
if !self.state.is_null() {
return true;
}
if let Some((st, origin)) = loader.init_state() {
self.state = st;
self.state_origin = Some(origin);
self.state_generation = loader.swap_generation();
true
} else {
false
}
}
fn drop_state(&mut self) {
if let (false, Some(origin)) = (self.state.is_null(), self.state_origin.take()) {
(origin.drop)(self.state);
}
self.state = std::ptr::null_mut();
}
fn reconcile_state(&mut self, loader: &NativeLoader<S>) -> bool {
let swap = loader.swap_generation();
if swap != self.state_generation {
let carried = if loader.preserve_dsp_state() && !self.state.is_null() {
self.state_origin.map(|o| (o.save)(self.state.cast_const()))
} else {
None
};
self.drop_state();
if self.ensure_state(loader) {
if let Some(bytes) = carried.filter(|b| !b.is_empty())
&& let Err(e) = loader.load_state(self.state, &bytes)
{
log::warn!(
"truce-hot: reloaded dylib rejected carried state, \
starting fresh: {e}"
);
}
let config = AudioConfig::new(self.sample_rate, self.max_block_size)
.with_process_mode(self.process_mode);
loader.reset(self.state, &config);
self.reset_pending = false;
}
self.state_generation = swap;
}
self.ensure_state(loader)
}
#[must_use]
pub fn meter_store(&self) -> Arc<truce_core::meters::MeterStore> {
Arc::clone(&self.meters)
}
#[must_use]
pub fn snapshot_slot(&self) -> Arc<truce_core::snapshot::SnapshotSlot> {
Arc::clone(&self.snapshots)
}
#[must_use]
pub fn editor_builder(&self) -> truce_core::editor::EditorBuilder<P> {
let loader = Arc::clone(&self.loader);
Box::new(move |params: Arc<P>| {
let params_ptr = Arc::as_ptr(¶ms).cast::<()>();
let guard = loader.try_lock_for(GUI_LOCK_WAIT)?;
guard.build_editor(params_ptr)
})
}
}
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, config: &AudioConfig) {
self.sample_rate = config.sample_rate;
self.max_block_size = config.max_block_size;
self.process_mode = config.process_mode;
self.params.set_sample_rate(config.sample_rate);
self.params.snap_smoothers();
let loader_arc = Arc::clone(&self.loader);
let Some(loader) = loader_arc.try_lock() else {
self.reset_pending = true;
return;
};
if self.reconcile_state(&loader) {
loader.reset(self.state, config);
self.latency_cache
.store(loader.latency(self.state), Ordering::Relaxed);
self.tail_cache
.store(loader.tail(self.state), Ordering::Relaxed);
self.reset_pending = false;
}
}
fn process(
&mut self,
buffer: &mut AudioBuffer<S>,
events: &EventList,
context: &mut ProcessContext,
) -> ProcessStatus {
let loader_arc = Arc::clone(&self.loader);
let Some(loader) = loader_arc.try_lock() else {
return ProcessStatus::Normal;
};
if !self.reconcile_state(&loader) {
return ProcessStatus::Normal;
}
if self.reset_pending {
let config = AudioConfig::new(self.sample_rate, self.max_block_size)
.with_process_mode(self.process_mode);
loader.reset(self.state, &config);
self.reset_pending = false;
}
for e in events.iter() {
if let EventBody::ParamChange { id, value } = &e.body {
self.params.set_plain(*id, *value);
}
}
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| meters.write(id, v);
let mut ctx = ProcessContext::new(
context.transport,
context.sample_rate,
buffer.num_samples(),
&mut *context.output_events,
)
.with_process_mode(context.process_mode)
.with_params(¶m_fn)
.with_meters(&meter_fn);
let status = loader.process(self.state, buffer, events, &mut ctx);
let state = self.state.cast_const();
let version = loader.snapshot_version(state);
crate::static_shell::publish_snapshot_with(
&self.snapshots,
&mut self.try_snapshot,
&mut self.last_snapshot_version,
version,
|buf| loader.snapshot_into(state, buf),
);
self.latency_cache
.store(loader.latency(state), Ordering::Relaxed);
self.tail_cache.store(loader.tail(state), Ordering::Relaxed);
status
}
fn save_state(&self) -> Vec<u8> {
let Some(_loader) = self.loader.try_lock_for(GUI_LOCK_WAIT) else {
return Vec::new();
};
match self.state_origin {
Some(origin) if !self.state.is_null() => (origin.save)(self.state.cast_const()),
_ => Vec::new(),
}
}
fn snapshot_into(&self, buf: &mut Vec<u8>) -> bool {
let Some(loader) = self.loader.try_lock_for(GUI_LOCK_WAIT) else {
return false;
};
if self.state.is_null() || loader.swap_generation() != self.state_generation {
return false;
}
loader.snapshot_into(self.state.cast_const(), buf)
}
fn republish_snapshot(&mut self) {
let loader_arc = Arc::clone(&self.loader);
let Some(loader) = loader_arc.try_lock_for(GUI_LOCK_WAIT) else {
return;
};
if !self.reconcile_state(&loader) {
return;
}
let state = self.state.cast_const();
let version = loader.snapshot_version(state);
crate::static_shell::publish_snapshot_with(
&self.snapshots,
&mut self.try_snapshot,
&mut self.last_snapshot_version,
version,
|buf| loader.snapshot_into(state, buf),
);
}
fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
let loader_arc = Arc::clone(&self.loader);
let Some(loader) = loader_arc.try_lock_for(GUI_LOCK_WAIT) else {
return Ok(());
};
if !self.reconcile_state(&loader) {
return Ok(());
}
let result = loader.load_state(self.state, data);
drop(loader);
self.last_snapshot_version = None;
result
}
fn migrate_state(
_foreign: &truce_core::state::ForeignState,
) -> Option<truce_core::state::MigratedState>
where
Self: Sized,
{
log::warn!(
"truce-hot: host offered foreign state but --shell builds don't \
route migrate_state; load will be reported as failed"
);
None
}
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 {
self.meters.read(meter_id)
}
}
impl<P: Params, S: Sample> Drop for HotShell<P, S> {
fn drop(&mut self) {
self.drop_state();
}
}