Skip to main content

truce_core/
plugin.rs

1use crate::buffer::AudioBuffer;
2use crate::bus::BusLayout;
3use crate::events::EventList;
4use crate::info::PluginInfo;
5use crate::process::{ProcessContext, ProcessStatus};
6use truce_params::sample::Sample;
7
8/// The format-facing plugin runtime trait. **Plugin authors do NOT
9/// implement this directly.**
10///
11/// `PluginRuntime` is the surface every format wrapper (CLAP, VST3,
12/// VST2, LV2, AU, AAX) consumes. The `truce::plugin!` macro generates
13/// an `impl PluginRuntime for __HotShellWrapper` from the user's
14/// `truce_plugin::PluginLogic` impl, bridging the user-facing trait
15/// into this GUI-free format-wrapper surface so `truce-core` doesn't
16/// pull in `truce-gui` types.
17///
18/// What plugin authors implement instead:
19///
20/// ```ignore
21/// impl truce::prelude::PluginLogic for MyPlugin {
22///     type Params = MyPluginParams;
23///     fn reset(&mut self, sr: f64, bs: usize) { /* ... */ }
24///     fn process(&mut self, /* ... */) -> ProcessStatus { /* ... */ }
25///     fn editor(params: Arc<MyPluginParams>) -> Box<dyn Editor> { /* ... */ }
26/// }
27///
28/// truce::plugin! { logic: MyPlugin, params: MyPluginParams }
29/// ```
30///
31/// The macro-emitted `impl PluginRuntime` routes each method directly
32/// to the user's impl.
33pub trait PluginRuntime: Send + 'static {
34    /// The plugin's chosen audio sample precision. Either `f32` (the
35    /// default - matches host wire format for nearly all formats) or
36    /// `f64` (for plugins whose DSP path runs in `f64` end-to-end:
37    /// high-order biquads, oscillator phase accumulators, long-running
38    /// cumulative state).
39    ///
40    /// The format wrapper bridges between host buffer precision and
41    /// `Self::Sample` at the block boundary - so the plugin's
42    /// `process()` always receives `AudioBuffer<Self::Sample>`
43    /// regardless of what the host sent. See
44    /// `truce_core::RawBufferScratch` for the conversion machinery.
45    ///
46    /// Drive this from the prelude: `truce::prelude` / `truce::prelude32`
47    /// implies `f32`, `truce::prelude64` implies `f64`. The
48    /// `truce::plugin!` macro emits `type Sample = …;` based on
49    /// which prelude is in scope at the macro call site.
50    type Sample: Sample;
51
52    /// Opt into zero-copy in-place I/O. When this returns `true`,
53    /// the format wrapper skips its safety memcpy on host-aliased
54    /// buffers and hands the plugin the raw shared memory through
55    /// `AudioBuffer::in_out_mut(ch)`. The plugin must check
56    /// `AudioBuffer::is_in_place(ch)` per channel before reading
57    /// `input(ch)` - for in-place channels `input(ch)` returns an
58    /// empty slice, and the data lives only in the shared buffer.
59    ///
60    /// Default `false`: the wrapper copies aliased inputs into scratch
61    /// so `input(ch)` and `output(ch)` are always disjoint. Costs one
62    /// memcpy per aliased channel per block (a few hundred KB/sec at
63    /// audio rates) and lets plugin code stay format-agnostic.
64    ///
65    /// `where Self: Sized` so a `dyn PluginRuntime` trait object stays
66    /// dyn-compatible - the format wrappers consume `P: PluginRuntime`
67    /// generically and call the method statically.
68    #[must_use]
69    fn supports_in_place() -> bool
70    where
71        Self: Sized,
72    {
73        false
74    }
75
76    /// Static metadata about the plugin.
77    ///
78    /// Use `plugin_info!()` for zero-boilerplate (reads from truce.toml
79    /// + Cargo.toml at compile time - no `build.rs` required).
80    fn info() -> PluginInfo
81    where
82        Self: Sized;
83
84    /// Supported bus layouts. The host picks one.
85    #[must_use]
86    fn bus_layouts() -> Vec<BusLayout>
87    where
88        Self: Sized,
89    {
90        vec![BusLayout::stereo()]
91    }
92
93    /// Called once after construction. Not real-time safe.
94    fn init(&mut self) {}
95
96    /// Called when sample rate or max block size changes.
97    /// Reset filters, delay lines, etc. Not real-time safe.
98    fn reset(&mut self, sample_rate: f64, max_block_size: usize);
99
100    /// Real-time audio processing.
101    fn process(
102        &mut self,
103        buffer: &mut AudioBuffer<Self::Sample>,
104        events: &EventList,
105        context: &mut ProcessContext,
106    ) -> ProcessStatus;
107
108    /// Save extra state beyond parameter values. Empty `Vec` means
109    /// "no extra state": matches the user-facing
110    /// `truce_plugin::PluginLogic::save_state` shape so the wrapper
111    /// bridge is a passthrough rather than an `Option<Vec<u8>>` to
112    /// `Vec<u8>` translation.
113    ///
114    /// **Concurrency contract.** Called on a host or GUI thread under
115    /// the wrapper's plugin lock, so it never runs concurrently with
116    /// `process()` - any field is safe to read. The flip side: an
117    /// audio block that arrives mid-save waits for this to return, so
118    /// keep it cheap (copy bytes out; don't compute or compress here).
119    fn save_state(&self) -> Vec<u8> {
120        Vec::new()
121    }
122
123    /// Restore extra state. Matches the user-facing
124    /// `truce_plugin::PluginLogic::load_state` `Result` shape so the
125    /// wrapper bridge is a passthrough.
126    ///
127    /// **Concurrency contract.** Called on the audio thread between
128    /// blocks (the wrappers queue host loads and apply them at the
129    /// top of `process()`), under the same exclusive access
130    /// `process()` has - any field is safe to write.
131    ///
132    /// # Errors
133    ///
134    /// Returns `Err` when the macro-generated impl forwards a
135    /// `PluginLogic::load_state` failure (malformed bytes, version
136    /// skew between session file and plugin build, etc).
137    fn load_state(&mut self, _data: &[u8]) -> Result<(), crate::state::StateLoadError> {
138        Ok(())
139    }
140
141    /// Translate foreign state - a previous framework's blob, or a
142    /// truce envelope saved under a different plugin id - into truce
143    /// params + extra. Format wrappers call this when the host hands
144    /// them state that isn't this plugin's envelope, so a plugin
145    /// ported to truce can keep its users' old sessions and presets.
146    ///
147    /// Pure and receiverless by design: it runs synchronously on the
148    /// host thread inside the wrapper's state callback (where parsing
149    /// a large legacy blob belongs), and taking no `self` means it
150    /// can't alias the audio thread's `&mut self`. The result rides
151    /// the normal restore pipeline; the next save writes a regular
152    /// envelope.
153    ///
154    /// Default: `None` - unrecognized state fails the load exactly
155    /// as it did before this hook existed.
156    #[must_use]
157    fn migrate_state(_foreign: &crate::state::ForeignState) -> Option<crate::state::MigratedState>
158    where
159        Self: Sized,
160    {
161        None
162    }
163
164    /// Processing latency in samples. Host uses this for delay compensation.
165    /// Return 0 if the plugin adds no latency (default).
166    fn latency(&self) -> u32 {
167        0
168    }
169
170    /// Tail time in samples. Return `u32::MAX` for infinite tail.
171    /// Return 0 for no tail (default).
172    fn tail(&self) -> u32 {
173        0
174    }
175
176    /// Read a meter value by ID (0.0–1.0).
177    ///
178    /// **Concurrency contract.** Shell-internal: format wrappers'
179    /// editor closures read meters through the shared
180    /// [`crate::meters::MeterStore`] handle
181    /// ([`crate::export::PluginExport::meter_store`]), never through
182    /// this method, so it has no cross-thread caller. It remains on
183    /// the trait for single-threaded consumers (the test driver, the
184    /// standalone's locked instance).
185    fn get_meter(&self, _meter_id: u32) -> f32 {
186        0.0
187    }
188}