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.
86 #[must_use]
87 fn bus_layouts() -> Vec<BusLayout>
88 where
89 Self: Sized,
90 {
91 vec![BusLayout::stereo()]
92 }
93
94 /// Called once after construction. Not real-time safe.
95 fn init(&mut self) {}
96
97 /// Called when sample rate, max block size, or processing mode
98 /// changes. Reset filters, delay lines, etc., and size any
99 /// mode-dependent buffers off `config.process_mode`. Not real-time
100 /// safe.
101 fn reset(&mut self, config: &AudioConfig);
102
103 /// Real-time audio processing.
104 fn process(
105 &mut self,
106 buffer: &mut AudioBuffer<Self::Sample>,
107 events: &EventList,
108 context: &mut ProcessContext,
109 ) -> ProcessStatus;
110
111 /// Save extra state beyond parameter values. Empty `Vec` means
112 /// "no extra state": matches the user-facing
113 /// `truce_plugin::PluginLogic::save_state` shape so the wrapper
114 /// bridge is a passthrough rather than an `Option<Vec<u8>>` to
115 /// `Vec<u8>` translation.
116 ///
117 /// **Concurrency contract.** Called on a host or GUI thread under
118 /// the wrapper's plugin lock, so it never runs concurrently with
119 /// `process()` - any field is safe to read. The flip side: an
120 /// audio block that arrives mid-save waits for this to return, so
121 /// keep it cheap (copy bytes out; don't compute or compress here).
122 fn save_state(&self) -> Vec<u8> {
123 Vec::new()
124 }
125
126 /// Restore extra state. Matches the user-facing
127 /// `truce_plugin::PluginLogic::load_state` `Result` shape so the
128 /// wrapper bridge is a passthrough.
129 ///
130 /// **Concurrency contract.** Called on the audio thread between
131 /// blocks (the wrappers queue host loads and apply them at the
132 /// top of `process()`), under the same exclusive access
133 /// `process()` has - any field is safe to write.
134 ///
135 /// # Errors
136 ///
137 /// Returns `Err` when the macro-generated impl forwards a
138 /// `PluginLogic::load_state` failure (malformed bytes, version
139 /// skew between session file and plugin build, etc).
140 fn load_state(&mut self, _data: &[u8]) -> Result<(), crate::state::StateLoadError> {
141 Ok(())
142 }
143
144 /// Translate foreign state - a previous framework's blob, or a
145 /// truce envelope saved under a different plugin id - into truce
146 /// params + extra. Format wrappers call this when the host hands
147 /// them state that isn't this plugin's envelope, so a plugin
148 /// ported to truce can keep its users' old sessions and presets.
149 ///
150 /// Pure and receiverless by design: it runs synchronously on the
151 /// host thread inside the wrapper's state callback (where parsing
152 /// a large legacy blob belongs), and taking no `self` means it
153 /// can't alias the audio thread's `&mut self`. The result rides
154 /// the normal restore pipeline; the next save writes a regular
155 /// envelope.
156 ///
157 /// Default: `None` - unrecognized state fails the load exactly
158 /// as it did before this hook existed.
159 #[must_use]
160 fn migrate_state(_foreign: &crate::state::ForeignState) -> Option<crate::state::MigratedState>
161 where
162 Self: Sized,
163 {
164 None
165 }
166
167 /// Processing latency in samples. Host uses this for delay compensation.
168 /// Return 0 if the plugin adds no latency (default).
169 fn latency(&self) -> u32 {
170 0
171 }
172
173 /// Tail time in samples. Return `u32::MAX` for infinite tail.
174 /// Return 0 for no tail (default).
175 fn tail(&self) -> u32 {
176 0
177 }
178
179 /// Read a meter value by ID (0.0–1.0).
180 ///
181 /// **Concurrency contract.** Shell-internal: format wrappers'
182 /// editor closures read meters through the shared
183 /// [`crate::meters::MeterStore`] handle
184 /// ([`crate::export::PluginExport::meter_store`]), never through
185 /// this method, so it has no cross-thread caller. It remains on
186 /// the trait for single-threaded consumers (the test driver, the
187 /// standalone's locked instance).
188 fn get_meter(&self, _meter_id: u32) -> f32 {
189 0.0
190 }
191}