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