use std::sync::Arc;
use truce_core::buffer::AudioBuffer;
use truce_core::bus::BusLayout;
use truce_core::config::AudioConfig;
use truce_core::events::{EventBody, EventList};
use truce_core::info::PluginInfo;
use truce_core::meters::MeterStore;
use truce_core::plugin::PluginRuntime;
use truce_core::process::{ProcessContext, ProcessStatus};
use truce_core::snapshot::SnapshotSlot;
use truce_core::state::{ForeignState, MigratedState, StateLoadError};
use truce_core::tasks::{AnyTaskSpawner, InitContext, warm_pool};
use truce_params::Params;
use truce_params::sample::Sample;
use truce_plugin::PluginLogicCore;
pub struct StaticShell<P: Params, L: PluginLogicCore<S, Params = P>, S: Sample = f32> {
pub params: Arc<P>,
state: L::DspState,
meters: Arc<MeterStore>,
snapshots: Arc<SnapshotSlot>,
try_snapshot: bool,
sample_rate: f64,
tasks: Option<AnyTaskSpawner>,
_sample: std::marker::PhantomData<fn() -> S>,
}
unsafe impl<P: Params, L: PluginLogicCore<S, Params = P>, S: Sample> Send for StaticShell<P, L, S> {}
impl<P: Params + Default + 'static, L: PluginLogicCore<S, Params = P> + 'static, S: Sample>
StaticShell<P, L, S>
{
pub fn from_parts(params: Arc<P>, tasks: Option<AnyTaskSpawner>) -> Self {
if tasks.is_some() {
warm_pool();
}
let init_ctx = InitContext::new(tasks.clone());
let state = L::init(¶ms, &init_ctx);
Self {
params,
state,
meters: MeterStore::new(),
snapshots: SnapshotSlot::new(),
try_snapshot: true,
sample_rate: 44100.0,
tasks,
_sample: std::marker::PhantomData,
}
}
pub fn meter_store(&self) -> Arc<MeterStore> {
Arc::clone(&self.meters)
}
pub fn snapshot_slot(&self) -> Arc<SnapshotSlot> {
Arc::clone(&self.snapshots)
}
pub fn task_spawner(&self) -> Option<AnyTaskSpawner> {
self.tasks.clone()
}
pub fn state_ref(&self) -> &L::DspState {
&self.state
}
pub fn state_ref_mut(&mut self) -> &mut L::DspState {
&mut self.state
}
}
impl<P: Params + Default + 'static, L: PluginLogicCore<S, Params = P> + 'static, S: Sample>
PluginRuntime for StaticShell<P, L, S>
{
type Sample = S;
fn info() -> PluginInfo
where
Self: Sized,
{
unreachable!("StaticShell::info() should not be called statically")
}
fn bus_layouts() -> Vec<BusLayout>
where
Self: Sized,
{
unreachable!("StaticShell::bus_layouts() should not be called statically")
}
fn init(&mut self) {}
fn reset(&mut self, config: &AudioConfig) {
self.sample_rate = config.sample_rate;
self.params.set_sample_rate(config.sample_rate);
self.params.snap_smoothers();
L::reset(&mut self.state, &self.params, config);
}
fn process(
&mut self,
buffer: &mut AudioBuffer<S>,
events: &EventList,
context: &mut ProcessContext,
) -> ProcessStatus {
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 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 mut ctx = match &self.tasks {
Some(t) => ctx.with_tasks(t),
None => ctx,
};
let status = L::process(&mut self.state, &self.params, buffer, events, &mut ctx);
publish_snapshot::<S, L>(&self.state, &self.snapshots, &mut self.try_snapshot);
status
}
fn save_state(&self) -> Vec<u8> {
L::save_state(&self.state)
}
fn republish_snapshot(&mut self) {
publish_snapshot::<S, L>(&self.state, &self.snapshots, &mut self.try_snapshot);
}
fn load_state(&mut self, data: &[u8]) -> Result<(), StateLoadError> {
let result = L::load_state(&mut self.state, data);
L::state_changed(&mut self.state, &self.params);
result
}
fn migrate_state(foreign: &ForeignState) -> Option<MigratedState>
where
Self: Sized,
{
<L as PluginLogicCore<S>>::migrate_state(foreign)
}
fn latency(&self) -> u32 {
L::latency(&self.state)
}
fn tail(&self) -> u32 {
L::tail(&self.state)
}
fn get_meter(&self, meter_id: u32) -> f32 {
self.meters.read(meter_id)
}
}
pub(crate) fn publish_snapshot<S, L>(
state: &L::DspState,
slot: &SnapshotSlot,
try_snapshot: &mut bool,
) where
S: Sample,
L: PluginLogicCore<S>,
{
publish_snapshot_with(slot, try_snapshot, |buf| L::snapshot_into(state, buf));
}
pub(crate) fn publish_snapshot_with(
slot: &SnapshotSlot,
try_snapshot: &mut bool,
snapshot_into: impl FnOnce(&mut Vec<u8>) -> bool,
) {
if !*try_snapshot {
return;
}
let ran_unsupported = std::cell::Cell::new(false);
slot.publish(|buf| {
let wrote = snapshot_into(buf);
ran_unsupported.set(!wrote);
wrote
});
if ran_unsupported.get() && !slot.is_supported() {
*try_snapshot = false;
}
}
#[macro_export]
macro_rules! export_static {
(
params: $params:ty,
info: $info:expr,
logic: $logic:ty,
$(tasks: [$($task:ty),+],)?
) => {
pub struct __HotShellWrapper {
inner: $crate::static_shell::StaticShell<$params, $logic, Sample>,
}
impl $crate::__macro_deps::truce_core::plugin::PluginRuntime for __HotShellWrapper {
type Sample = Sample;
fn supports_in_place() -> bool
where
Self: Sized,
{
<$logic as $crate::__macro_deps::truce_plugin::PluginLogicCore<Sample>>::supports_in_place()
}
fn info() -> $crate::__macro_deps::truce_core::info::PluginInfo
where
Self: Sized,
{
$info
}
fn bus_layouts() -> Vec<$crate::__macro_deps::truce_core::bus::BusLayout>
where
Self: Sized,
{
<$logic as $crate::__macro_deps::truce_plugin::PluginLogicCore<Sample>>::bus_layouts()
}
fn init(&mut self) {
self.inner.init();
}
fn reset(&mut self, config: &$crate::__macro_deps::truce_core::config::AudioConfig) {
self.inner.reset(config);
}
fn process(
&mut self,
buffer: &mut $crate::__macro_deps::truce_core::buffer::AudioBuffer<Sample>,
events: &$crate::__macro_deps::truce_core::events::EventList,
context: &mut $crate::__macro_deps::truce_core::process::ProcessContext,
) -> $crate::__macro_deps::truce_core::process::ProcessStatus {
self.inner.process(buffer, events, context)
}
fn save_state(&self) -> Vec<u8> {
self.inner.save_state()
}
fn load_state(
&mut self,
data: &[u8],
) -> Result<(), $crate::__macro_deps::truce_core::state::StateLoadError> {
self.inner.load_state(data)
}
fn migrate_state(
foreign: &$crate::__macro_deps::truce_core::state::ForeignState,
) -> Option<$crate::__macro_deps::truce_core::state::MigratedState>
where
Self: Sized,
{
<$logic as $crate::__macro_deps::truce_plugin::PluginLogicCore<Sample>>::migrate_state(foreign)
}
fn latency(&self) -> u32 {
self.inner.latency()
}
fn tail(&self) -> u32 {
self.inner.tail()
}
fn get_meter(&self, meter_id: u32) -> f32 {
self.inner.get_meter(meter_id)
}
}
impl $crate::__macro_deps::truce_core::export::PluginExport for __HotShellWrapper {
type Params = $params;
fn create() -> Self {
let params = std::sync::Arc::new(<$params>::new());
#[allow(unused_mut)]
let mut __task_bundle =
$crate::__macro_deps::truce_core::tasks::TaskSpawnerBundle::new();
$(
$({
let __task_run = {
let params = std::sync::Arc::clone(¶ms);
move |task| {
<$task as $crate::__macro_deps::truce_plugin::BackgroundTask>::run(
task, ¶ms,
)
}
};
let __spawner = if <$task as $crate::__macro_deps::truce_plugin::BackgroundTask>::SERIALIZED {
$crate::__macro_deps::truce_core::tasks::TaskSpawner::<$task>::new_serialized(__task_run)
} else {
$crate::__macro_deps::truce_core::tasks::TaskSpawner::<$task>::new(__task_run)
};
__task_bundle.push(__spawner);
})+
)?
let tasks = __task_bundle.into_any();
Self {
inner: $crate::static_shell::StaticShell::from_parts(params, tasks),
}
}
fn params(&self) -> &$params {
&self.inner.params
}
fn params_arc(&self) -> std::sync::Arc<$params> {
std::sync::Arc::clone(&self.inner.params)
}
fn meter_store(
&self,
) -> std::sync::Arc<$crate::__macro_deps::truce_core::meters::MeterStore> {
self.inner.meter_store()
}
fn snapshot_slot(
&self,
) -> std::sync::Arc<$crate::__macro_deps::truce_core::snapshot::SnapshotSlot> {
self.inner.snapshot_slot()
}
fn task_spawner(
&self,
) -> ::core::option::Option<
$crate::__macro_deps::truce_core::tasks::AnyTaskSpawner,
> {
self.inner.task_spawner()
}
fn editor_builder(
&self,
) -> $crate::__macro_deps::truce_core::editor::EditorBuilder<$params> {
Box::new(|params| {
Some(
<$logic as $crate::__macro_deps::truce_plugin::PluginEditor<Sample>>::editor(
params,
),
)
})
}
}
};
}
#[cfg(test)]
mod tests {
use super::publish_snapshot_with;
use truce_core::snapshot::SnapshotSlot;
#[test]
fn non_opt_in_latches_off_on_first_block() {
let slot = SnapshotSlot::new();
let mut try_snapshot = true;
publish_snapshot_with(&slot, &mut try_snapshot, |_| false);
assert!(!try_snapshot, "first false must latch off");
assert!(!slot.is_supported());
let mut called = false;
publish_snapshot_with(&slot, &mut try_snapshot, |_| {
called = true;
false
});
assert!(!called, "latched-off slot must not call snapshot_into");
}
#[test]
fn opt_in_then_contract_violation_stays_subscribed() {
let slot = SnapshotSlot::new();
let mut try_snapshot = true;
publish_snapshot_with(&slot, &mut try_snapshot, |buf| {
buf.clear();
buf.extend_from_slice(&[1, 2, 3]);
true
});
assert!(try_snapshot);
assert!(slot.is_supported());
assert_eq!(slot.read(), Some(vec![1, 2, 3]));
publish_snapshot_with(&slot, &mut try_snapshot, |_| false);
assert!(try_snapshot, "a post-opt-in false must not latch off");
publish_snapshot_with(&slot, &mut try_snapshot, |buf| {
buf.clear();
buf.extend_from_slice(&[4]);
true
});
assert_eq!(slot.read(), Some(vec![4]));
}
}