Skip to main content

truce_core/
plugin.rs

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