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::StateLoadError;
46use truce_gui_types::interaction::WidgetRegion;
47use truce_gui_types::layout::GridLayout;
48use truce_gui_types::render::RenderBackend;
49use truce_gui_types::widgets::WidgetType;
50use truce_params::sample::Sample;
51
52// ---------------------------------------------------------------------------
53// PluginLogicCore - generic trait, what format wrappers consume
54// ---------------------------------------------------------------------------
55
56/// Wrapper-facing plugin trait, generic over the audio sample type.
57///
58/// Format wrappers (`StaticShell`, `HotShell`, CLAP / VST3 / etc.)
59/// bind on `PluginLogicCore<S>`. Plugin authors don't implement this
60/// directly - they implement [`PluginLogic`] (`f32`) or
61/// [`PluginLogic64`] (`f64`), and the blanket impls below route them
62/// into `PluginLogicCore`.
63///
64/// Method docs live on the leaf traits ([`PluginLogic`] /
65/// [`PluginLogic64`]); the shape mirrors them exactly.
66pub trait PluginLogicCore<S: Sample = f32>: Send + 'static {
67    #[must_use]
68    fn supports_in_place() -> bool
69    where
70        Self: Sized;
71
72    #[must_use]
73    fn bus_layouts() -> Vec<BusLayout>
74    where
75        Self: Sized;
76
77    fn reset(&mut self, sample_rate: f64, max_block_size: usize);
78
79    fn process(
80        &mut self,
81        buffer: &mut AudioBuffer<S>,
82        events: &EventList,
83        context: &mut ProcessContext,
84    ) -> ProcessStatus;
85
86    fn save_state(&self) -> Vec<u8>;
87    /// Restore plugin-specific state. See [`PluginLogic::load_state`].
88    ///
89    /// # Errors
90    ///
91    /// Forwards whatever the user impl returns - typically a malformed
92    /// blob error decoded by `bincode` / `serde` / similar.
93    fn load_state(&mut self, data: &[u8]) -> Result<(), StateLoadError>;
94    fn state_changed(&mut self);
95    fn latency(&self) -> u32;
96    fn tail(&self) -> u32;
97    fn layout(&self) -> GridLayout;
98    fn render(&self, backend: &mut dyn RenderBackend);
99    fn uses_custom_render(&self) -> bool;
100    fn hit_test(&self, widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize>;
101    fn custom_editor(&self) -> Option<Box<dyn Editor>>;
102}
103
104// ---------------------------------------------------------------------------
105// Leaf traits - what plugin authors implement
106// ---------------------------------------------------------------------------
107
108/// Define a sample-pinned leaf trait. Two invocations:
109/// `PluginLogic` (f32) and [`PluginLogic64`] (f64). The trait
110/// definition has to be a macro because we want the two trait
111/// surfaces to stay in exact lock-step - adding a new method means
112/// updating one place, not three (the macro, plus two trait
113/// declarations).
114///
115/// Doc-hidden because it's a single-purpose internal macro, not an
116/// API users should reach for.
117#[doc(hidden)]
118#[macro_export]
119macro_rules! plugin_logic_leaf_trait {
120    ($(#[$attr:meta])* $vis:vis trait $name:ident<sample = $sample:ty>) => {
121        $(#[$attr])*
122        $vis trait $name: Send + 'static {
123            /// Opt into zero-copy in-place I/O. When this returns `true`,
124            /// the format wrapper skips its safety memcpy on host-aliased
125            /// buffers and hands the plugin the raw shared memory through
126            /// `AudioBuffer::in_out_mut(ch)`. The plugin must check
127            /// `AudioBuffer::is_in_place(ch)` per channel before reading
128            /// `input(ch)`.
129            ///
130            /// Default `false`: the wrapper copies aliased inputs into
131            /// scratch so `input(ch)` and `output(ch)` are always
132            /// disjoint. Costs one memcpy per aliased channel per block.
133            #[must_use]
134            fn supports_in_place() -> bool
135            where
136                Self: Sized,
137            {
138                false
139            }
140
141            /// Supported audio bus configurations. The host picks one;
142            /// the others are rejected at bus-config time before
143            /// `process` is ever called. Default: stereo in, stereo out.
144            #[must_use]
145            fn bus_layouts() -> Vec<$crate::__plugin_logic_deps::BusLayout>
146            where
147                Self: Sized,
148            {
149                vec![$crate::__plugin_logic_deps::BusLayout::stereo()]
150            }
151
152            /// Reset for a new sample rate / block size. Called before
153            /// the first `process` and any time the host reconfigures.
154            fn reset(&mut self, sample_rate: f64, max_block_size: usize);
155
156            /// Process one block of audio. Real-time - no allocations,
157            /// locks, or I/O.
158            fn process(
159                &mut self,
160                buffer: &mut $crate::__plugin_logic_deps::AudioBuffer<$sample>,
161                events: &$crate::__plugin_logic_deps::EventList,
162                context: &mut $crate::__plugin_logic_deps::ProcessContext,
163            ) -> $crate::__plugin_logic_deps::ProcessStatus;
164
165            /// Serialize plugin-specific state (DSP state, not params -
166            /// those are saved automatically). Default: no extra state.
167            fn save_state(&self) -> Vec<u8> {
168                Vec::new()
169            }
170
171            /// Restore plugin-specific state.
172            ///
173            /// # Errors
174            ///
175            /// Return `Err(StateLoadError)` when the blob is malformed
176            /// or otherwise can't be interpreted - the format wrapper
177            /// logs the failure (and on hosts that support it, surfaces
178            /// it to the DAW).
179            fn load_state(
180                &mut self,
181                _data: &[u8],
182            ) -> Result<(), $crate::__plugin_logic_deps::StateLoadError> {
183                Ok(())
184            }
185
186            /// Called on the audio thread immediately after
187            /// [`Self::load_state`] returns. Invalidate or recompute any
188            /// caches the next `process()` reads. Default: no-op.
189            fn state_changed(&mut self) {}
190
191            /// Report latency in samples for plugin delay compensation.
192            fn latency(&self) -> u32 {
193                0
194            }
195
196            /// Report tail time in samples (audio produced after input
197            /// stops - reverbs, delays). `u32::MAX` for infinite tail.
198            fn tail(&self) -> u32 {
199                0
200            }
201
202            // ---- GUI ----
203
204            /// Return the widget layout for the built-in GUI. Default:
205            /// empty layout. Plugins that supply a custom editor via
206            /// [`Self::custom_editor`] can leave this default.
207            fn layout(&self) -> $crate::__plugin_logic_deps::GridLayout {
208                $crate::__plugin_logic_deps::GridLayout::build(vec![])
209            }
210
211            /// Render the GUI into a backend. Default: no-op. Override
212            /// only for custom GPU/CPU rasterisation outside the
213            /// standard widget set; flip [`Self::uses_custom_render`]
214            /// to `true` when you do.
215            fn render(&self, _backend: &mut dyn $crate::__plugin_logic_deps::RenderBackend) {}
216
217            /// Whether this plugin overrides [`Self::render`]. The
218            /// shell uses the standard widget drawing from
219            /// [`Self::layout`] when this is `false`. Default: `false`.
220            fn uses_custom_render(&self) -> bool {
221                false
222            }
223
224            /// Hit test: which widget (if any) is at `(x, y)`?
225            /// Default: circular for knobs, rectangular for everything
226            /// else, meters skipped.
227            fn hit_test(
228                &self,
229                widgets: &[$crate::__plugin_logic_deps::WidgetRegion],
230                x: f32,
231                y: f32,
232            ) -> Option<usize> {
233                $crate::__plugin_logic_deps::default_hit_test(widgets, x, y)
234            }
235
236            /// Provide a custom [`Editor`] instead of the built-in
237            /// widget layout (egui, iced, slint, raw window handle).
238            /// The shell calls this first; if it returns `None`, falls
239            /// back to the built-in editor from [`Self::layout`].
240            fn custom_editor(&self) -> Option<Box<dyn $crate::__plugin_logic_deps::Editor>> {
241                None
242            }
243        }
244    };
245}
246
247// Re-export the dependencies the leaf-trait macro substitutes by path,
248// under one `pub` doc-hidden module so user crates that invoke the
249// macro don't need to import each truce-core type by hand.
250#[doc(hidden)]
251pub mod __plugin_logic_deps {
252    pub use truce_core::buffer::AudioBuffer;
253    pub use truce_core::bus::BusLayout;
254    pub use truce_core::editor::Editor;
255    pub use truce_core::events::EventList;
256    pub use truce_core::process::{ProcessContext, ProcessStatus};
257    pub use truce_core::state::StateLoadError;
258
259    pub use truce_gui_types::interaction::WidgetRegion;
260    pub use truce_gui_types::layout::GridLayout;
261    pub use truce_gui_types::render::RenderBackend;
262
263    pub use crate::default_hit_test;
264}
265
266plugin_logic_leaf_trait! {
267    /// The `f32`-buffer user-facing plugin trait.
268    ///
269    /// Plugin authors implement this in a single `impl` block when
270    /// their audio path is `f32` end-to-end (the default - matches
271    /// the host wire format for nearly all DAWs and formats).
272    /// `truce::prelude` and `truce::prelude32` re-export this name
273    /// directly; `truce::prelude64m` does too (the `m` mixed-precision
274    /// prelude keeps the audio buffer at `f32` and only switches the
275    /// `param.read()` precision).
276    ///
277    /// Only [`Self::reset`] and [`Self::process`] are required;
278    /// everything else has a default. Headless (no-GUI) plugins leave
279    /// `layout` / `render` / `custom_editor` at their defaults - the
280    /// format wrappers fall back to a minimal built-in editor.
281    pub trait PluginLogic<sample = f32>
282}
283
284plugin_logic_leaf_trait! {
285    /// The `f64`-buffer user-facing plugin trait. Same surface as
286    /// [`PluginLogic`] but with the audio buffer pinned to `f64`.
287    ///
288    /// Plugin authors don't usually name this directly - `truce::prelude64`
289    /// re-exports it as `PluginLogic`, so the impl header reads the
290    /// same regardless of which precision the prelude chose. Pick
291    /// `truce::prelude64` (and thus this leaf) when the DSP path runs
292    /// in `f64` end-to-end and the wrapper-boundary widen/narrow
293    /// memcpy is worth the cleaner DSP code.
294    pub trait PluginLogic64<sample = f64>
295}
296
297// ---------------------------------------------------------------------------
298// Bridges - each leaf forwards every method to PluginLogicCore<S>
299// ---------------------------------------------------------------------------
300
301/// Define a blanket `impl<T: $leaf> PluginLogicCore<$sample> for T`
302/// that forwards every trait method to `<T as $leaf>::method(...)`.
303/// One source-of-truth for both `(PluginLogic, f32)` and
304/// `(PluginLogic64, f64)` bridges.
305macro_rules! plugin_logic_bridge {
306    ($leaf:ident, $sample:ty) => {
307        impl<T: $leaf> PluginLogicCore<$sample> for T {
308            fn supports_in_place() -> bool
309            where
310                Self: Sized,
311            {
312                <Self as $leaf>::supports_in_place()
313            }
314
315            fn bus_layouts() -> Vec<BusLayout>
316            where
317                Self: Sized,
318            {
319                <Self as $leaf>::bus_layouts()
320            }
321
322            fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
323                <Self as $leaf>::reset(self, sample_rate, max_block_size);
324            }
325
326            fn process(
327                &mut self,
328                buffer: &mut AudioBuffer<$sample>,
329                events: &EventList,
330                context: &mut ProcessContext,
331            ) -> ProcessStatus {
332                // FTZ/DAZ (or FZ on AArch64) for the duration of
333                // the user's process body. Denormals on filter
334                // feedback paths stall the core; the guard pays
335                // ~two MXCSR writes per block to avoid that.
336                let _denormal_guard = DenormalGuard::new();
337                <Self as $leaf>::process(self, buffer, events, context)
338            }
339
340            fn save_state(&self) -> Vec<u8> {
341                <Self as $leaf>::save_state(self)
342            }
343
344            fn load_state(&mut self, data: &[u8]) -> Result<(), StateLoadError> {
345                <Self as $leaf>::load_state(self, data)
346            }
347
348            fn state_changed(&mut self) {
349                <Self as $leaf>::state_changed(self);
350            }
351
352            fn latency(&self) -> u32 {
353                <Self as $leaf>::latency(self)
354            }
355
356            fn tail(&self) -> u32 {
357                <Self as $leaf>::tail(self)
358            }
359
360            fn layout(&self) -> GridLayout {
361                <Self as $leaf>::layout(self)
362            }
363
364            fn render(&self, backend: &mut dyn RenderBackend) {
365                <Self as $leaf>::render(self, backend);
366            }
367
368            fn uses_custom_render(&self) -> bool {
369                <Self as $leaf>::uses_custom_render(self)
370            }
371
372            fn hit_test(&self, widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
373                <Self as $leaf>::hit_test(self, widgets, x, y)
374            }
375
376            fn custom_editor(&self) -> Option<Box<dyn Editor>> {
377                <Self as $leaf>::custom_editor(self)
378            }
379        }
380    };
381}
382
383plugin_logic_bridge!(PluginLogic, f32);
384plugin_logic_bridge!(PluginLogic64, f64);
385
386// ---------------------------------------------------------------------------
387// Default hit test - referenced by leaf macro expansions
388// ---------------------------------------------------------------------------
389
390/// Default hit test: circular for knobs, rectangular for everything
391/// else, skip meters. Used by the leaf traits' `hit_test` defaults.
392#[must_use]
393pub fn default_hit_test(widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
394    for (i, w) in widgets.iter().enumerate() {
395        if w.widget_type == WidgetType::Meter {
396            continue;
397        }
398        if w.widget_type == WidgetType::Knob {
399            let dx = x - w.cx;
400            let dy = y - w.cy;
401            if dx * dx + dy * dy <= w.radius * w.radius {
402                return Some(i);
403            }
404        } else if x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h {
405            return Some(i);
406        }
407    }
408    None
409}