truce_rack_core/plugin.rs
1//! The host-facing [`Plugin`] trait and its supporting
2//! per-block types.
3//!
4//! Format wrappers (`truce-rack-clap`, `truce-rack-vst3`, `truce-rack-au`, …)
5//! implement [`Plugin`] for their per-format instance type. Host
6//! applications then hold a `Box<dyn Plugin<S>>` (or a generic
7//! `<P: Plugin<S>>`) without caring which format produced it.
8
9use crate::buffer::AudioBuffer;
10use crate::bus::BusLayout;
11use crate::error::Result;
12use crate::events::EventList;
13use crate::info::{ParameterInfo, PluginInfo, PresetInfo};
14use crate::sample::Sample;
15use crate::transport::TransportInfo;
16
17/// Per-block context carrying host state into `process` and
18/// returning per-block side-channel data from it.
19///
20/// Plugins write outbound events (MIDI thru, parameter touches)
21/// into `output_events`; the wrapper drains them after the call
22/// returns. Hosts that don't care about outbound events pass an
23/// empty list and ignore whatever the plugin pushes.
24pub struct ProcessContext<'a> {
25 /// Sample rate active for this block. Plugins should
26 /// recompute coefficients when this changes between blocks
27 /// (rare but legal — host sample-rate change without a
28 /// full deactivate / activate cycle).
29 pub sample_rate: f64,
30 /// Maximum frames the plugin was prepared for. The buffer's
31 /// `num_frames()` may be less; never more.
32 pub max_block_size: usize,
33 /// Host transport snapshot for this block. `None` when the
34 /// host doesn't expose transport (most CLAP hosts via the
35 /// optional `clap.transport` extension only on hosts that
36 /// support it).
37 pub transport: Option<TransportInfo>,
38 /// Outbound event sink the plugin pushes parameter touches /
39 /// MIDI thru into. Cleared by the wrapper at the start of
40 /// each block.
41 pub output_events: &'a mut EventList,
42}
43
44/// Hint from the plugin about whether more output is coming.
45///
46/// Mirrors CLAP's `clap_process_status`. Hosts use the hint to
47/// decide whether to keep calling `process` on an idle channel.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum ProcessStatus {
50 /// Normal output — the plugin has more work in subsequent
51 /// blocks regardless of input. Default for live processing.
52 Continue,
53 /// The plugin has no output and won't produce any until
54 /// fresh input or events arrive. Host may skip `process`
55 /// calls until then.
56 Sleep,
57 /// Tail-out — the plugin will keep producing audio for
58 /// `tail_samples` more samples even with silent input
59 /// (reverb, delay).
60 Tail {
61 /// Remaining tail length in samples.
62 tail_samples: u32,
63 },
64 /// Hard error during processing. Wrapper logs and the host
65 /// should treat the block's output as garbage (silence is a
66 /// safer fallback for live audio).
67 Error,
68}
69
70/// Core sample-precision-erased interface every plugin exposes.
71///
72/// Methods that don't touch audio samples are here so a host can
73/// query metadata before deciding whether to instantiate as
74/// `Plugin<f32>` or `Plugin<f64>`. Mirrors truce's
75/// `PluginLogicCore` shape — the leaf [`Plugin<S>`] adds the
76/// sample-typed `process`.
77pub trait PluginCore: Send {
78 /// Plugin metadata as the wrapper scanned it.
79 fn info(&self) -> &PluginInfo;
80
81 /// The bus layout currently active. `None` until
82 /// [`PluginCore::activate`] picks one.
83 fn active_layout(&self) -> Option<&BusLayout>;
84
85 /// All bus layouts the plugin supports. The host picks one
86 /// and passes it to `activate`. Returned by reference into
87 /// internally-cached metadata; cheap to call repeatedly.
88 fn supported_layouts(&self) -> &[BusLayout];
89
90 /// Number of parameters this plugin exposes.
91 fn parameter_count(&self) -> usize;
92
93 /// Metadata for parameter at `index` (0-based into the
94 /// plugin's declared list).
95 ///
96 /// # Errors
97 /// Returns [`crate::Error::InvalidParameter`] when `index >=
98 /// parameter_count()`.
99 fn parameter_info(&self, index: usize) -> Result<ParameterInfo>;
100
101 /// Current value of parameter `index` in its native unit.
102 ///
103 /// # Errors
104 /// Returns [`crate::Error::InvalidParameter`] when out of
105 /// range or [`crate::Error::NotActivated`] when called before
106 /// `activate`.
107 fn parameter_value(&self, index: usize) -> Result<f64>;
108
109 /// Format the parameter value at `index` as the plugin
110 /// would render it in its own UI. Many formats supply this
111 /// directly (`clap_param_info_value_to_text`); others
112 /// require host-side formatting.
113 ///
114 /// # Errors
115 /// Returns [`crate::Error::InvalidParameter`] when `index` is
116 /// out of range or [`crate::Error::NotActivated`] when called
117 /// before `activate`.
118 fn parameter_value_string(&self, index: usize, value: f64) -> Result<String>;
119
120 /// Set parameter `index` to `value` in native units. Set
121 /// outside `process` (this is the host-thread setter); the
122 /// plugin may smooth toward the new value over subsequent
123 /// blocks.
124 ///
125 /// # Errors
126 /// Returns [`crate::Error::InvalidParameter`] when out of
127 /// range or [`crate::Error::NotActivated`] when called before
128 /// `activate`.
129 fn set_parameter(&mut self, index: usize, value: f64) -> Result<()>;
130
131 /// Number of factory presets, if the plugin exposes any.
132 fn preset_count(&self) -> usize;
133
134 /// Metadata for preset at `index`.
135 ///
136 /// # Errors
137 /// Returns [`crate::Error::InvalidParameter`] when out of
138 /// range.
139 fn preset_info(&self, index: usize) -> Result<PresetInfo>;
140
141 /// Load preset by the format-specific id from
142 /// [`PresetInfo::preset_number`].
143 ///
144 /// # Errors
145 /// Wrapper-specific — typically when the id is unknown.
146 fn load_preset(&mut self, preset_number: i32) -> Result<()>;
147
148 /// Snapshot plugin state to a byte blob. Wrap in
149 /// [`crate::StateEnvelope`] before persisting if the host
150 /// wants the version / format header.
151 ///
152 /// # Errors
153 /// Wrapper-specific.
154 fn save_state(&self) -> Result<Vec<u8>>;
155
156 /// Restore plugin state from bytes previously returned by
157 /// [`PluginCore::save_state`]. The host strips its own
158 /// envelope before calling this — the bytes here are
159 /// plugin-opaque.
160 ///
161 /// # Errors
162 /// Wrapper-specific.
163 fn load_state(&mut self, bytes: &[u8]) -> Result<()>;
164
165 /// Pick a bus layout and prepare the plugin for processing
166 /// at `sample_rate` with blocks up to `max_block_size`
167 /// frames.
168 ///
169 /// Hosts must call `activate` before any `process` call.
170 /// Subsequent reconfiguration (sample-rate change, layout
171 /// switch) requires a `deactivate` + `activate` cycle.
172 ///
173 /// # Errors
174 /// Wrapper-specific — typically when the requested layout
175 /// isn't in [`PluginCore::supported_layouts`].
176 fn activate(
177 &mut self,
178 layout: BusLayout,
179 sample_rate: f64,
180 max_block_size: usize,
181 ) -> Result<()>;
182
183 /// Tear down the active processing config. After this call
184 /// the plugin holds no per-activation resources and `process`
185 /// won't be called until the next `activate`.
186 fn deactivate(&mut self);
187
188 /// `true` when [`PluginCore::activate`] has been called and
189 /// [`PluginCore::deactivate`] hasn't been called since.
190 fn is_active(&self) -> bool;
191
192 /// Borrow the plugin's editor controller if the plugin
193 /// exposes a custom GUI. Returns `None` for headless plugins
194 /// or plugins whose editor extension is missing.
195 ///
196 /// The returned reference borrows `&mut self`, which means the
197 /// host can't call `process` (which also needs `&mut self`)
198 /// while holding it. That's the Rust-level enforcement of the
199 /// "audio thread vs UI thread" discipline.
200 fn editor(&mut self) -> Option<&mut dyn crate::editor::PluginEditor> {
201 None
202 }
203}
204
205/// Sample-precision-typed leaf trait. Pairs `PluginCore` with the
206/// `process` callback at a specific sample type.
207///
208/// Most format wrappers implement `Plugin<f32>`; ones that
209/// support host-chosen 64-bit (VST3, AU v2/v3, AAX) implement
210/// both `Plugin<f32>` and `Plugin<f64>` on the same instance
211/// type or on a precision-specialised wrapper.
212pub trait Plugin<S: Sample>: PluginCore {
213 /// Process one audio block.
214 ///
215 /// # Real-time-safety contract
216 ///
217 /// This callback runs on the host's audio thread. The
218 /// wrapper guarantees no allocator-touching work inside this
219 /// crate on the call edge; the *plugin* code itself is
220 /// expected to honor the same: no `Box::new`, no
221 /// `Vec::push` past pre-grown capacity, no mutex locking,
222 /// no I/O, no `panic!`. A panic is caught by the wrapper
223 /// (see [`crate::wrapper::run_audio_block_with`]) and turned
224 /// into [`ProcessStatus::Error`] but the host's block is
225 /// still lost.
226 ///
227 /// # Errors
228 /// Returns [`crate::Error::NotActivated`] when the plugin
229 /// hasn't been activated; wrapper-specific errors otherwise.
230 fn process(
231 &mut self,
232 buffer: &mut AudioBuffer<'_, S>,
233 events: &EventList,
234 context: &mut ProcessContext<'_>,
235 ) -> Result<ProcessStatus>;
236}