Skip to main content

truce_plugin/
lib.rs

1//! User-facing plugin traits + internal bridge.
2//!
3//! This crate is the plugin author's entry point. The single
4//! `impl PluginLogic for MyPlugin { ... }` block covers both
5//! audio-thread DSP and main-thread GUI, with sample precision
6//! routed through the prelude (see `truce::prelude` /
7//! `truce::prelude64`).
8//!
9//! `truce-plugin` depends on `truce-gui-types` (light: layout,
10//! render trait, widget regions) - not the full `truce-gui`.
11//! Plugin authors who supply a custom editor (egui, iced, slint,
12//! raw window handle) end up with `truce-plugin` in their dep
13//! tree but not the built-in editor's tiny-skia + baseview +
14//! truce-font stack.
15//!
16//! ## Three traits, one source of truth
17//!
18//! - [`PluginLogic`]   - what plugin authors implement for `f32`-buffer plugins.
19//! - [`PluginLogic64`] - what plugin authors implement for `f64`-buffer plugins.
20//! - [`PluginLogicCore`] - generic-over-`S` trait the format wrappers consume.
21//!
22//! The two leaf traits are stamped from one
23//! `plugin_logic_leaf_trait!` `macro_rules!` definition (further
24//! down this file) so their method surfaces stay in lock-step. Each leaf
25//! gets a blanket impl that forwards every method to
26//! `PluginLogicCore<S>` with the matching `S`. Wrappers
27//! (`StaticShell`, `HotShell`, the format crates) bind on
28//! `PluginLogicCore<S>` and don't care which leaf the user impl'd.
29//!
30//! ## What this buys
31//!
32//! Plugin authors writing `impl PluginLogic for Synth { ... }`
33//! never name a precision. The `truce::prelude64` re-export aliases
34//! `PluginLogic64` as `PluginLogic` in the user's scope, so the
35//! same impl header reads the same regardless of which prelude is
36//! in use. The `<S>` token that used to live on the impl header is
37//! gone - the prelude carries the precision choice.
38
39use truce_core::buffer::AudioBuffer;
40use truce_core::bus::BusLayout;
41use truce_core::denormal::DenormalGuard;
42use truce_core::editor::Editor;
43use truce_core::events::EventList;
44use truce_core::process::{ProcessContext, ProcessStatus};
45use truce_core::state::{ForeignState, MigratedState, StateLoadError};
46use truce_gui_types::interaction::WidgetRegion;
47use truce_gui_types::widgets::WidgetType;
48use truce_params::sample::Sample;
49
50// ---------------------------------------------------------------------------
51// PluginLogicCore - generic trait, what format wrappers consume
52// ---------------------------------------------------------------------------
53
54/// Wrapper-facing plugin trait, generic over the audio sample type.
55///
56/// Format wrappers (`StaticShell`, `HotShell`, CLAP / VST3 / etc.)
57/// bind on `PluginLogicCore<S>`. Plugin authors don't implement this
58/// directly - they implement [`PluginLogic`] (`f32`) or
59/// [`PluginLogic64`] (`f64`), and the blanket impls below route them
60/// into `PluginLogicCore`.
61///
62/// Method docs live on the leaf traits ([`PluginLogic`] /
63/// [`PluginLogic64`]); the shape mirrors them exactly.
64pub trait PluginLogicCore<S: Sample = f32>: Send + 'static {
65    #[must_use]
66    fn supports_in_place() -> bool
67    where
68        Self: Sized;
69
70    #[must_use]
71    fn bus_layouts() -> Vec<BusLayout>
72    where
73        Self: Sized;
74
75    fn reset(&mut self, sample_rate: f64, max_block_size: usize);
76
77    fn process(
78        &mut self,
79        buffer: &mut AudioBuffer<S>,
80        events: &EventList,
81        context: &mut ProcessContext,
82    ) -> ProcessStatus;
83
84    fn save_state(&self) -> Vec<u8>;
85    /// Restore plugin-specific state. See [`PluginLogic::load_state`].
86    ///
87    /// # Errors
88    ///
89    /// Forwards whatever the user impl returns - typically a malformed
90    /// blob error decoded by `bincode` / `serde` / similar.
91    fn load_state(&mut self, data: &[u8]) -> Result<(), StateLoadError>;
92    /// Translate foreign state into truce shape. See
93    /// [`PluginLogic::migrate_state`].
94    #[must_use]
95    fn migrate_state(foreign: &ForeignState) -> Option<MigratedState>
96    where
97        Self: Sized;
98    fn state_changed(&mut self);
99    fn latency(&self) -> u32;
100    fn tail(&self) -> u32;
101    /// Construct the editor for this plugin. Required - there is no
102    /// auto-fallback. Layout-only plugins call
103    /// `truce_gui::default_editor(params, layout)` from here; custom-
104    /// renderer plugins construct their `EguiEditor` / `IcedEditor` /
105    /// `SlintEditor` / hand-rolled `Editor` directly.
106    fn editor(&self) -> Box<dyn Editor>;
107}
108
109// ---------------------------------------------------------------------------
110// Leaf traits - what plugin authors implement
111// ---------------------------------------------------------------------------
112
113/// Define a sample-pinned leaf trait. Two invocations:
114/// `PluginLogic` (f32) and [`PluginLogic64`] (f64). The trait
115/// definition has to be a macro because we want the two trait
116/// surfaces to stay in exact lock-step - adding a new method means
117/// updating one place, not three (the macro, plus two trait
118/// declarations).
119///
120/// Doc-hidden because it's a single-purpose internal macro, not an
121/// API users should reach for.
122#[doc(hidden)]
123#[macro_export]
124macro_rules! plugin_logic_leaf_trait {
125    ($(#[$attr:meta])* $vis:vis trait $name:ident<sample = $sample:ty>) => {
126        $(#[$attr])*
127        $vis trait $name: Send + 'static {
128            /// Opt into zero-copy in-place I/O. When this returns `true`,
129            /// the format wrapper skips its safety memcpy on host-aliased
130            /// buffers and hands the plugin the raw shared memory through
131            /// `AudioBuffer::in_out_mut(ch)`. The plugin must check
132            /// `AudioBuffer::is_in_place(ch)` per channel before reading
133            /// `input(ch)`.
134            ///
135            /// Default `false`: the wrapper copies aliased inputs into
136            /// scratch so `input(ch)` and `output(ch)` are always
137            /// disjoint. Costs one memcpy per aliased channel per block.
138            #[must_use]
139            fn supports_in_place() -> bool
140            where
141                Self: Sized,
142            {
143                false
144            }
145
146            /// Supported audio bus configurations. The host picks one;
147            /// the others are rejected at bus-config time before
148            /// `process` is ever called. Default: stereo in, stereo out.
149            #[must_use]
150            fn bus_layouts() -> Vec<$crate::__plugin_logic_deps::BusLayout>
151            where
152                Self: Sized,
153            {
154                vec![$crate::__plugin_logic_deps::BusLayout::stereo()]
155            }
156
157            /// Reset for a new sample rate / block size. Called before
158            /// the first `process` and any time the host reconfigures.
159            fn reset(&mut self, sample_rate: f64, max_block_size: usize);
160
161            /// Process one block of audio. Real-time - no allocations,
162            /// locks, or I/O.
163            fn process(
164                &mut self,
165                buffer: &mut $crate::__plugin_logic_deps::AudioBuffer<$sample>,
166                events: &$crate::__plugin_logic_deps::EventList,
167                context: &mut $crate::__plugin_logic_deps::ProcessContext,
168            ) -> $crate::__plugin_logic_deps::ProcessStatus;
169
170            /// Serialize plugin-specific state (DSP state, not params -
171            /// those are saved automatically). Default: no extra state.
172            ///
173            /// Runs on a host or GUI thread while the audio thread is
174            /// paused at a block boundary (the wrapper's plugin lock),
175            /// so reading any field is safe - but an audio block that
176            /// arrives mid-save waits for this to return. Keep it
177            /// cheap: copy bytes out, don't compute or compress here.
178            fn save_state(&self) -> Vec<u8> {
179                Vec::new()
180            }
181
182            /// Restore plugin-specific state.
183            ///
184            /// Runs on the audio thread between blocks, with the same
185            /// exclusive access `process()` has - writing any field
186            /// is safe.
187            ///
188            /// # Errors
189            ///
190            /// Return `Err(StateLoadError)` when the blob is malformed
191            /// or otherwise can't be interpreted - the format wrapper
192            /// logs the failure (and on hosts that support it, surfaces
193            /// it to the DAW).
194            fn load_state(
195                &mut self,
196                _data: &[u8],
197            ) -> Result<(), $crate::__plugin_logic_deps::StateLoadError> {
198                Ok(())
199            }
200
201            /// Called on the audio thread immediately after
202            /// [`Self::load_state`] returns. Invalidate or recompute any
203            /// caches the next `process()` reads. Default: no-op.
204            fn state_changed(&mut self) {}
205
206            /// Translate foreign state - a previous framework's blob,
207            /// or a truce envelope saved under a different plugin id -
208            /// into truce params + extra, so a plugin ported to truce
209            /// keeps its users' old sessions and presets. Runs on the
210            /// host thread; receiverless so it can't touch (or alias)
211            /// the live instance. Return `None` for bytes you don't
212            /// recognize - the wrapper then reports load failure to
213            /// the host, exactly as if this hook didn't exist.
214            ///
215            /// One-shot by construction: the next save writes a normal
216            /// truce envelope, so this never becomes a permanent
217            /// dual-format reader. Keyed formats (AU / LV2 / AAX) only
218            /// see foreign bytes when `truce.toml` declares the legacy
219            /// keys to probe (`[plugin.legacy_state]`).
220            #[must_use]
221            fn migrate_state(
222                _foreign: &$crate::__plugin_logic_deps::ForeignState,
223            ) -> Option<$crate::__plugin_logic_deps::MigratedState>
224            where
225                Self: Sized,
226            {
227                None
228            }
229
230            /// Report latency in samples for plugin delay compensation.
231            fn latency(&self) -> u32 {
232                0
233            }
234
235            /// Report tail time in samples (audio produced after input
236            /// stops - reverbs, delays). `u32::MAX` for infinite tail.
237            fn tail(&self) -> u32 {
238                0
239            }
240
241            // ---- GUI ----
242
243            /// Construct the editor for this plugin. Required.
244            ///
245            /// There is no auto-fallback - every plugin explicitly
246            /// names which renderer it wants. For the built-in
247            /// widget layout, call
248            /// `truce_gui::default_editor(params, layout)`; for
249            /// custom renderers, construct an `EguiEditor` /
250            /// `IcedEditor` / `SlintEditor` / hand-rolled `Editor`
251            /// here. The choice of renderer crate the plugin's
252            /// `Cargo.toml` pulls IS the choice of editor.
253            fn editor(&self) -> Box<dyn $crate::__plugin_logic_deps::Editor>;
254        }
255    };
256}
257
258// Re-export the dependencies the leaf-trait macro substitutes by path,
259// under one `pub` doc-hidden module so user crates that invoke the
260// macro don't need to import each truce-core type by hand.
261#[doc(hidden)]
262pub mod __plugin_logic_deps {
263    pub use truce_core::buffer::AudioBuffer;
264    pub use truce_core::bus::BusLayout;
265    pub use truce_core::editor::Editor;
266    pub use truce_core::events::EventList;
267    pub use truce_core::process::{ProcessContext, ProcessStatus};
268    pub use truce_core::state::{ForeignState, MigratedState, StateLoadError};
269}
270
271plugin_logic_leaf_trait! {
272    /// The `f32`-buffer user-facing plugin trait.
273    ///
274    /// Plugin authors implement this in a single `impl` block when
275    /// their audio path is `f32` end-to-end (the default - matches
276    /// the host wire format for nearly all DAWs and formats).
277    /// `truce::prelude` and `truce::prelude32` re-export this name
278    /// directly; `truce::prelude64m` does too (the `m` mixed-precision
279    /// prelude keeps the audio buffer at `f32` and only switches the
280    /// `param.read()` precision).
281    ///
282    /// Required: [`Self::reset`], [`Self::process`], [`Self::editor`].
283    /// Everything else has a default. The editor is constructed
284    /// explicitly - layout-only plugins typically call
285    /// `truce_gui::default_editor(self.params.clone(), self.layout())`
286    /// (where `layout()` is a plain inherent method on the plugin
287    /// struct, not part of the trait).
288    pub trait PluginLogic<sample = f32>
289}
290
291plugin_logic_leaf_trait! {
292    /// The `f64`-buffer user-facing plugin trait. Same surface as
293    /// [`PluginLogic`] but with the audio buffer pinned to `f64`.
294    ///
295    /// Plugin authors don't usually name this directly - `truce::prelude64`
296    /// re-exports it as `PluginLogic`, so the impl header reads the
297    /// same regardless of which precision the prelude chose. Pick
298    /// `truce::prelude64` (and thus this leaf) when the DSP path runs
299    /// in `f64` end-to-end and the wrapper-boundary widen/narrow
300    /// memcpy is worth the cleaner DSP code.
301    pub trait PluginLogic64<sample = f64>
302}
303
304// ---------------------------------------------------------------------------
305// Bridges - each leaf forwards every method to PluginLogicCore<S>
306// ---------------------------------------------------------------------------
307
308/// Define a blanket `impl<T: $leaf> PluginLogicCore<$sample> for T`
309/// that forwards every trait method to `<T as $leaf>::method(...)`.
310/// One source-of-truth for both `(PluginLogic, f32)` and
311/// `(PluginLogic64, f64)` bridges.
312macro_rules! plugin_logic_bridge {
313    ($leaf:ident, $sample:ty) => {
314        impl<T: $leaf> PluginLogicCore<$sample> for T {
315            fn supports_in_place() -> bool
316            where
317                Self: Sized,
318            {
319                <Self as $leaf>::supports_in_place()
320            }
321
322            fn bus_layouts() -> Vec<BusLayout>
323            where
324                Self: Sized,
325            {
326                <Self as $leaf>::bus_layouts()
327            }
328
329            fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
330                <Self as $leaf>::reset(self, sample_rate, max_block_size);
331            }
332
333            fn process(
334                &mut self,
335                buffer: &mut AudioBuffer<$sample>,
336                events: &EventList,
337                context: &mut ProcessContext,
338            ) -> ProcessStatus {
339                // FTZ/DAZ (or FZ on AArch64) for the duration of
340                // the user's process body. Denormals on filter
341                // feedback paths stall the core; the guard pays
342                // ~two MXCSR writes per block to avoid that.
343                let _denormal_guard = DenormalGuard::new();
344                <Self as $leaf>::process(self, buffer, events, context)
345            }
346
347            fn save_state(&self) -> Vec<u8> {
348                <Self as $leaf>::save_state(self)
349            }
350
351            fn load_state(&mut self, data: &[u8]) -> Result<(), StateLoadError> {
352                <Self as $leaf>::load_state(self, data)
353            }
354
355            fn state_changed(&mut self) {
356                <Self as $leaf>::state_changed(self);
357            }
358
359            fn migrate_state(foreign: &ForeignState) -> Option<MigratedState>
360            where
361                Self: Sized,
362            {
363                <Self as $leaf>::migrate_state(foreign)
364            }
365
366            fn latency(&self) -> u32 {
367                <Self as $leaf>::latency(self)
368            }
369
370            fn tail(&self) -> u32 {
371                <Self as $leaf>::tail(self)
372            }
373
374            fn editor(&self) -> Box<dyn Editor> {
375                <Self as $leaf>::editor(self)
376            }
377        }
378    };
379}
380
381plugin_logic_bridge!(PluginLogic, f32);
382plugin_logic_bridge!(PluginLogic64, f64);
383
384// ---------------------------------------------------------------------------
385// Default hit test - referenced by leaf macro expansions
386// ---------------------------------------------------------------------------
387
388/// Default hit test: circular for knobs, rectangular for everything
389/// else, skip meters. Used by the leaf traits' `hit_test` defaults.
390#[must_use]
391pub fn default_hit_test(widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
392    for (i, w) in widgets.iter().enumerate() {
393        if w.widget_type == WidgetType::Meter {
394            continue;
395        }
396        if w.widget_type == WidgetType::Knob {
397            let dx = x - w.cx;
398            let dy = y - w.cy;
399            if dx * dx + dy * dy <= w.radius * w.radius {
400                return Some(i);
401            }
402        } else if x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h {
403            return Some(i);
404        }
405    }
406    None
407}