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//! Plus one layer of sugar: [`PurePluginLogic`] / [`PurePluginLogic64`]
23//! for plugins with no DSP state, blanket-implemented into the
24//! matching leaf so everything downstream sees a normal `PluginLogic`
25//! with `DspState = ()`.
26//!
27//! The two leaf traits are stamped from one
28//! `plugin_logic_leaf_trait!` `macro_rules!` definition (further
29//! down this file) so their method surfaces stay in lock-step. Each leaf
30//! gets a blanket impl that forwards every method to
31//! `PluginLogicCore<S>` with the matching `S`. Wrappers
32//! (`StaticShell`, `HotShell`, the format crates) bind on
33//! `PluginLogicCore<S>` and don't care which leaf the user impl'd.
34//!
35//! ## What this buys
36//!
37//! Plugin authors writing `impl PluginLogic for Synth { ... }`
38//! never name a precision. The `truce::prelude64` re-export aliases
39//! `PluginLogic64` as `PluginLogic` in the user's scope, so the
40//! same impl header reads the same regardless of which prelude is
41//! in use. The `<S>` token that used to live on the impl header is
42//! gone - the prelude carries the precision choice.
43
44use truce_core::buffer::AudioBuffer;
45use truce_core::bus::BusLayout;
46use truce_core::config::AudioConfig;
47use truce_core::denormal::DenormalGuard;
48use truce_core::editor::Editor;
49use truce_core::events::EventList;
50use truce_core::process::{ProcessContext, ProcessStatus};
51use truce_core::state::{ForeignState, MigratedState, StateLoadError};
52use truce_gui_types::interaction::WidgetRegion;
53use truce_gui_types::widgets::WidgetType;
54use truce_params::sample::Sample;
55
56// ---------------------------------------------------------------------------
57// PluginLogicCore - generic trait, what format wrappers consume
58// ---------------------------------------------------------------------------
59
60/// Wrapper-facing plugin trait, generic over the audio sample type.
61///
62/// Format wrappers (`StaticShell`, `HotShell`, CLAP / VST3 / etc.)
63/// bind on `PluginLogicCore<S>`. Plugin authors don't implement this
64/// directly - they implement [`PluginLogic`] (`f32`) or
65/// [`PluginLogic64`] (`f64`), and the blanket impls below route them
66/// into `PluginLogicCore`.
67///
68/// Method docs live on the leaf traits ([`PluginLogic`] /
69/// [`PluginLogic64`]); the shape mirrors them exactly.
70pub trait PluginLogicCore<S: Sample = f32>: 'static {
71    /// The plugin's parameter struct; mirrors the leaf's `Params`.
72    type Params: truce_params::Params;
73    /// The mutable per-block audio state. Owned by the shell, not by
74    /// `Self` (the descriptor). `Send` because the shell moves it across
75    /// threads; `'static` because the shell may outlive any borrow. No
76    /// layout trait is required: the hot-reload shell fingerprints the
77    /// type at load time from its `type_name` / `size_of` / `align_of`.
78    type DspState: Send + 'static;
79
80    /// Whether the hot-reload shell may preserve live DSP state across a
81    /// code-only reload. Default `true` (best-effort layout probe).
82    /// Override to `false` on a state that must always re-init on reload.
83    const PRESERVE_DSP_STATE: bool = true;
84
85    #[must_use]
86    fn supports_in_place() -> bool {
87        false
88    }
89
90    #[must_use]
91    fn bus_layouts() -> Vec<BusLayout> {
92        vec![BusLayout::stereo()]
93    }
94
95    /// Build initial state from params. See [`PluginLogic::init`].
96    fn init(params: &Self::Params) -> Self::DspState;
97
98    fn reset(state: &mut Self::DspState, params: &Self::Params, config: &AudioConfig);
99
100    fn process(
101        state: &mut Self::DspState,
102        params: &Self::Params,
103        buffer: &mut AudioBuffer<S>,
104        events: &EventList,
105        context: &mut ProcessContext,
106    ) -> ProcessStatus;
107
108    fn save_state(state: &Self::DspState) -> Vec<u8> {
109        let _ = state;
110        Vec::new()
111    }
112    /// Lock-free state-save opt-in. See [`PluginLogic::snapshot_into`].
113    fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool {
114        let _ = (state, buf);
115        false
116    }
117    /// Restore plugin-specific state. See [`PluginLogic::load_state`].
118    ///
119    /// # Errors
120    ///
121    /// Forwards whatever the user impl returns - typically a malformed
122    /// blob error decoded by `bincode` / `serde` / similar.
123    fn load_state(state: &mut Self::DspState, data: &[u8]) -> Result<(), StateLoadError> {
124        let _ = (state, data);
125        Ok(())
126    }
127    /// Translate foreign state into truce shape. See
128    /// [`PluginLogic::migrate_state`].
129    #[must_use]
130    fn migrate_state(foreign: &ForeignState) -> Option<MigratedState> {
131        let _ = foreign;
132        None
133    }
134    fn state_changed(state: &mut Self::DspState, params: &Self::Params) {
135        let _ = (state, params);
136    }
137    fn latency(state: &Self::DspState) -> u32 {
138        let _ = state;
139        0
140    }
141    fn tail(state: &Self::DspState) -> u32 {
142        let _ = state;
143        0
144    }
145}
146
147/// Precision-keyed editor factory, bridged from the leaf traits.
148///
149/// `plugin!` / `export_static!` / `export_plugin!` build the editor from
150/// the concrete logic type without naming which leaf trait
151/// ([`PluginLogic`] vs [`PluginLogic64`]) it implements. Keyed on `S`
152/// only so the two per-leaf blanket impls don't overlap - the editor and
153/// param store are precision-independent.
154///
155/// This lives off [`PluginLogicCore`] on purpose: it carries an
156/// associated `Params` type and a receiverless `editor`, either of which
157/// would make `PluginLogicCore` non-object-safe and break the
158/// hot-reload loader's type-erased `Box<dyn PluginLogicCore<S>>`. Only
159/// concrete code (the shells' macros) ever names it, never `dyn`.
160pub trait PluginEditor<S: Sample> {
161    /// The plugin's parameter struct; mirrors the leaf's `Params`.
162    type Params: truce_params::Params;
163
164    /// Build the editor from the lock-free param store. Receiverless, so
165    /// the wrapper constructs it while the audio thread runs, without the
166    /// plugin lock.
167    fn editor(params: std::sync::Arc<Self::Params>) -> Box<dyn Editor>;
168}
169
170// ---------------------------------------------------------------------------
171// Leaf traits - what plugin authors implement
172// ---------------------------------------------------------------------------
173
174/// Define a sample-pinned leaf trait. Two invocations:
175/// `PluginLogic` (f32) and [`PluginLogic64`] (f64). The trait
176/// definition has to be a macro because we want the two trait
177/// surfaces to stay in exact lock-step - adding a new method means
178/// updating one place, not three (the macro, plus two trait
179/// declarations).
180///
181/// Doc-hidden because it's a single-purpose internal macro, not an
182/// API users should reach for.
183#[doc(hidden)]
184#[macro_export]
185macro_rules! plugin_logic_leaf_trait {
186    ($(#[$attr:meta])* $vis:vis trait $name:ident<sample = $sample:ty>) => {
187        $(#[$attr])*
188        $vis trait $name: 'static {
189            /// The plugin's parameter struct (`#[derive(Params)]`).
190            /// Shared, immutable during a block - it arrives by
191            /// reference every call from the shell, which owns the
192            /// `Arc`. Never stored in [`Self::DspState`].
193            type Params: $crate::__plugin_logic_deps::Params;
194
195            /// The mutable per-block audio state - filter memory, voice
196            /// buffers, phase accumulators. A plain struct, distinct from
197            /// the descriptor `Self` (except for the small-effect
198            /// `type DspState = Self` shape). A plugin with no audio state
199            /// implements the stateless leaf trait instead of this one and
200            /// never names a `DspState`. Owned by the shell, so it can
201            /// outlive a code swap.
202            /// `Default` is how the state is born: the default
203            /// [`Self::init`] returns `Self::DspState::default()`, so most
204            /// plugins never write `init` - they `#[derive(Default)]` (or
205            /// hand-write `Default` when a fresh state has non-zero fields)
206            /// and override `init` only when construction needs params.
207            /// No layout trait is required - the hot-reload shell
208            /// fingerprints the type at load time from its `type_name` /
209            /// `size_of` / `align_of`.
210            type DspState: ::core::default::Default + Send + 'static;
211
212            /// Whether the hot-reload shell may preserve live DSP state
213            /// across a code-only reload. Default `true`: the shell keeps
214            /// the state when a best-effort layout probe (`type_name` +
215            /// `size_of` + `align_of`) matches, so a reverb tail survives
216            /// an edit-and-reload, and re-inits when it differs. Set to
217            /// `false` on a state that must always re-init on reload.
218            const PRESERVE_DSP_STATE: bool = true;
219
220            /// Opt into zero-copy in-place I/O. When this returns `true`,
221            /// the format wrapper skips its safety memcpy on host-aliased
222            /// buffers and hands the plugin the raw shared memory through
223            /// `AudioBuffer::in_out_mut(ch)`. The plugin must check
224            /// `AudioBuffer::is_in_place(ch)` per channel before reading
225            /// `input(ch)`.
226            ///
227            /// Default `false`: the wrapper copies aliased inputs into
228            /// scratch so `input(ch)` and `output(ch)` are always
229            /// disjoint. Costs one memcpy per aliased channel per block.
230            #[must_use]
231            fn supports_in_place() -> bool {
232                false
233            }
234
235            /// Supported audio bus configurations. The host picks one;
236            /// the others are rejected at bus-config time before
237            /// `process` is ever called. Default: stereo in, stereo out.
238            #[must_use]
239            fn bus_layouts() -> Vec<$crate::__plugin_logic_deps::BusLayout> {
240                vec![$crate::__plugin_logic_deps::BusLayout::stereo()]
241            }
242
243            /// Build the initial audio state from params. Replaces the
244            /// old `new` constructor: the descriptor is stateless, so
245            /// state is born here and owned by the shell. Not real-time
246            /// safe - allocate freely.
247            ///
248            /// Default: `Self::DspState::default()`. Override only when
249            /// construction genuinely needs to read params; a fixed
250            /// initial state belongs in the state type's `Default` impl
251            /// instead.
252            fn init(params: &Self::Params) -> Self::DspState {
253                let _ = params;
254                ::core::default::Default::default()
255            }
256
257            /// Reset for a new sample rate / block size / processing
258            /// mode. Clear `state`'s filters / delay lines; read
259            /// `config.process_mode` to size buffers for an offline
260            /// render (allocation belongs here, off the audio thread) -
261            /// see [`AudioConfig`](truce_core::config::AudioConfig).
262            ///
263            /// Params plumbing is NOT your job: the shell calls
264            /// `params.set_sample_rate(config.sample_rate)` and
265            /// `params.snap_smoothers()` before invoking this, so the
266            /// body only handles state the plugin itself owns. Default:
267            /// no-op, right for a stateless plugin (`DspState = ()`).
268            fn reset(
269                state: &mut Self::DspState,
270                params: &Self::Params,
271                config: &$crate::__plugin_logic_deps::AudioConfig,
272            ) {
273                let _ = (state, params, config);
274            }
275
276            /// Process one block of audio. Real-time - no allocations,
277            /// locks, or I/O. `state` is exclusively owned this block;
278            /// `params` is shared and immutable.
279            fn process(
280                state: &mut Self::DspState,
281                params: &Self::Params,
282                buffer: &mut $crate::__plugin_logic_deps::AudioBuffer<$sample>,
283                events: &$crate::__plugin_logic_deps::EventList,
284                context: &mut $crate::__plugin_logic_deps::ProcessContext,
285            ) -> $crate::__plugin_logic_deps::ProcessStatus;
286
287            /// Serialize plugin-specific state (DSP state, not params -
288            /// those are saved automatically). Default: delegates to
289            /// [`Self::snapshot_into`] (empty when neither is
290            /// overridden).
291            ///
292            /// Runs on a host or GUI thread while the audio thread is
293            /// paused at a block boundary (the wrapper's plugin lock),
294            /// so reading any field is safe - but an audio block that
295            /// arrives mid-save waits for this to return. Keep it
296            /// cheap: copy bytes out, don't compute or compress here.
297            /// To take this off the plugin lock entirely, override
298            /// [`Self::snapshot_into`] instead.
299            fn save_state(state: &Self::DspState) -> Vec<u8> {
300                let mut buf = Vec::new();
301                let _ = Self::snapshot_into(state, &mut buf);
302                buf
303            }
304
305            /// Opt into lock-free state save. `buf` arrives **cleared**,
306            /// with its capacity retained across calls so a steady state
307            /// is allocation-free; fill it with the same bytes
308            /// [`Self::save_state`] would produce (append freely - it is
309            /// never carried over from the previous block).
310            ///
311            /// The return value is a *static capability*, not a
312            /// per-block flag: `true` means "this plugin publishes
313            /// snapshots", `false` means "it never does" (the default).
314            /// Once you return `true` you must return `true` for the
315            /// plugin's whole lifetime - if the custom state empties out,
316            /// return `true` with `buf` left empty (an empty blob), don't
317            /// return `false`. The shell latches the opt-in on the first
318            /// published block; a later `false` is a contract violation
319            /// that would otherwise leave the host reading a stale
320            /// snapshot forever.
321            ///
322            /// Called on the **audio thread** after each process block,
323            /// under the same real-time rules as `process` - bounded, no
324            /// unbounded allocation. The wrapper publishes the result
325            /// into a lock-free slot the host reads without ever taking
326            /// the plugin lock, so saving state while audio runs never
327            /// stalls the audio thread. Overriding this is the
328            /// preferred way to serialize custom state; the default
329            /// [`Self::save_state`] delegates here.
330            fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool {
331                let _ = (state, buf);
332                false
333            }
334
335            /// Restore plugin-specific state into `state`.
336            ///
337            /// Runs on the audio thread between blocks, with the same
338            /// exclusive access `process()` has - writing any field
339            /// is safe.
340            ///
341            /// # Errors
342            ///
343            /// Return `Err(StateLoadError)` when the blob is malformed
344            /// or otherwise can't be interpreted - the format wrapper
345            /// logs the failure (and on hosts that support it, surfaces
346            /// it to the DAW).
347            fn load_state(
348                state: &mut Self::DspState,
349                data: &[u8],
350            ) -> Result<(), $crate::__plugin_logic_deps::StateLoadError> {
351                let _ = (state, data);
352                Ok(())
353            }
354
355            /// Called on the audio thread immediately after
356            /// [`Self::load_state`] returns. Invalidate or recompute any
357            /// caches in `state` that the next `process()` reads. Default:
358            /// no-op.
359            fn state_changed(state: &mut Self::DspState, params: &Self::Params) {
360                let _ = (state, params);
361            }
362
363            /// Translate foreign state - a previous framework's blob,
364            /// or a truce envelope saved under a different plugin id -
365            /// into truce params + extra, so a plugin ported to truce
366            /// keeps its users' old sessions and presets. Runs on the
367            /// host thread; receiverless so it can't touch (or alias)
368            /// the live instance. Return `None` for bytes you don't
369            /// recognize - the wrapper then reports load failure to
370            /// the host, exactly as if this hook didn't exist.
371            ///
372            /// One-shot by construction: the next save writes a normal
373            /// truce envelope, so this never becomes a permanent
374            /// dual-format reader. Keyed formats (AU / LV2 / AAX) only
375            /// see foreign bytes when `truce.toml` declares the legacy
376            /// keys to probe (`[plugin.legacy_state]`).
377            #[must_use]
378            fn migrate_state(
379                foreign: &$crate::__plugin_logic_deps::ForeignState,
380            ) -> Option<$crate::__plugin_logic_deps::MigratedState> {
381                let _ = foreign;
382                None
383            }
384
385            /// Report latency in samples for plugin delay compensation.
386            /// May change at runtime - return a new value and the host is
387            /// notified (see the wrapper latency-change path).
388            fn latency(state: &Self::DspState) -> u32 {
389                let _ = state;
390                0
391            }
392
393            /// Report tail time in samples (audio produced after input
394            /// stops - reverbs, delays). `u32::MAX` for infinite tail.
395            fn tail(state: &Self::DspState) -> u32 {
396                let _ = state;
397                0
398            }
399
400            // ---- GUI ----
401
402            /// Construct the editor for this plugin. Required.
403            ///
404            /// There is no auto-fallback - every plugin explicitly
405            /// names which renderer it wants. For the built-in
406            /// widget layout, call
407            /// `truce_gui::default_editor(params, layout)`; for
408            /// custom renderers, construct an `EguiEditor` /
409            /// `IcedEditor` / `SlintEditor` / hand-rolled `Editor`
410            /// here. The choice of renderer crate the plugin's
411            /// `Cargo.toml` pulls IS the choice of editor.
412            ///
413            /// An associated function, not a method: it receives the
414            /// lock-free `Arc<Self::Params>` store the wrapper already
415            /// holds, so the host can open the editor while audio is
416            /// running without ever taking the plugin lock. Editors bind
417            /// only to the param store (plus meters / transport, all
418            /// lock-free); custom DSP state is read at runtime through
419            /// the editor bridge, not at construction.
420            fn editor(
421                params: ::std::sync::Arc<Self::Params>,
422            ) -> Box<dyn $crate::__plugin_logic_deps::Editor>;
423        }
424    };
425}
426
427// Re-export the dependencies the leaf-trait macro substitutes by path,
428// under one `pub` doc-hidden module so user crates that invoke the
429// macro don't need to import each truce-core type by hand.
430#[doc(hidden)]
431pub mod __plugin_logic_deps {
432    pub use truce_core::buffer::AudioBuffer;
433    pub use truce_core::bus::BusLayout;
434    pub use truce_core::config::AudioConfig;
435    pub use truce_core::dsp_state::{NO_PRESERVE, layout_fingerprint};
436    pub use truce_core::editor::Editor;
437    pub use truce_core::events::EventList;
438    pub use truce_core::process::{ProcessContext, ProcessStatus};
439    pub use truce_core::state::{ForeignState, MigratedState, StateLoadError};
440    pub use truce_params::Params;
441}
442
443plugin_logic_leaf_trait! {
444    /// The `f32`-buffer user-facing plugin trait.
445    ///
446    /// Plugin authors implement this in a single `impl` block when
447    /// their audio path is `f32` end-to-end (the default - matches
448    /// the host wire format for nearly all DAWs and formats).
449    /// `truce::prelude` and `truce::prelude32` re-export this name
450    /// directly; `truce::prelude64m` does too (the `m` mixed-precision
451    /// prelude keeps the audio buffer at `f32` and only switches the
452    /// `param.read()` precision).
453    ///
454    /// Required: [`Self::process`], [`Self::editor`]. Everything else
455    /// has a default: `init` builds `Self::DspState::default()` unless
456    /// construction needs params, and `reset` is a no-op. The editor is
457    /// constructed explicitly - layout-only plugins typically call
458    /// `truce_gui::default_editor(params, layout())` (where `layout()`
459    /// is a plain inherent method on the plugin struct, not part of the
460    /// trait). A plugin with no DSP state at all should implement
461    /// [`PurePluginLogic`] instead and skip the state plumbing entirely.
462    ///
463    /// ## Params vs. DSP state
464    ///
465    /// The type you implement this on is a stateless descriptor; the
466    /// data lives in two places, and the method signatures reflect the
467    /// split:
468    ///
469    /// - **Params** (`type Params`) - the user-facing values in your
470    ///   `#[derive(Params)]` struct, held as `Arc<Self::Params>`.
471    ///   Atomic-backed and `Sync`, shared lock-free with the host and
472    ///   the editor. Arrives read-only as `&Self::Params`.
473    /// - **DSP state** (`type DspState`) - filter memory, phase
474    ///   accumulators, voice buffers, delay lines. Plain and
475    ///   non-atomic, mutated every sample, exclusive to the audio
476    ///   thread. Owned by the shell, passed `&mut` to the methods that
477    ///   mutate it (`process` / `reset` / `load_state`) and `&` to the
478    ///   ones that read it (`save_state` / `snapshot_into`).
479    ///
480    /// `editor` takes neither - it is an associated function over the
481    /// param store, because a GUI is a *view* that binds only params
482    /// (plus lock-free meters / transport) and never touches DSP state,
483    /// so it can be built without the plugin lock. DSP state can't move
484    /// into params: making per-sample filter memory atomic-shared would
485    /// put a synchronized access on the hottest path, and it isn't a
486    /// "parameter" anyway.
487    pub trait PluginLogic<sample = f32>
488}
489
490plugin_logic_leaf_trait! {
491    /// The `f64`-buffer user-facing plugin trait. Same surface as
492    /// [`PluginLogic`] but with the audio buffer pinned to `f64`.
493    ///
494    /// Plugin authors don't usually name this directly - `truce::prelude64`
495    /// re-exports it as `PluginLogic`, so the impl header reads the
496    /// same regardless of which precision the prelude chose. Pick
497    /// `truce::prelude64` (and thus this leaf) when the DSP path runs
498    /// in `f64` end-to-end and the wrapper-boundary widen/narrow
499    /// memcpy is worth the cleaner DSP code.
500    pub trait PluginLogic64<sample = f64>
501}
502
503// ---------------------------------------------------------------------------
504// Pure leaf traits - stateless sugar over PluginLogic / PluginLogic64
505// ---------------------------------------------------------------------------
506
507/// Define a sample-pinned pure (stateless) leaf trait plus its blanket
508/// impl into the matching stateful leaf. Two invocations: `PurePluginLogic`
509/// over [`PluginLogic`] and [`PurePluginLogic64`] over [`PluginLogic64`].
510/// A macro for the same reason as [`plugin_logic_leaf_trait!`]: the two
511/// surfaces stay in lock-step by construction.
512macro_rules! pure_plugin_leaf_trait {
513    ($(#[$attr:meta])* $vis:vis trait $name:ident: $leaf:ident<sample = $sample:ty>) => {
514        $(#[$attr])*
515        $vis trait $name: 'static {
516            /// The plugin's parameter struct (`#[derive(Params)]`).
517            /// Shared, immutable during a block - it arrives by
518            /// reference every call from the shell, which owns the
519            /// `Arc`.
520            type Params: crate::__plugin_logic_deps::Params;
521
522            /// Opt into zero-copy in-place I/O. Same contract as the
523            /// stateful leaf's `supports_in_place`.
524            #[must_use]
525            fn supports_in_place() -> bool {
526                false
527            }
528
529            /// Supported audio bus configurations. Same contract as the
530            /// stateful leaf's `bus_layouts`. Default: stereo in,
531            /// stereo out.
532            #[must_use]
533            fn bus_layouts() -> Vec<crate::__plugin_logic_deps::BusLayout> {
534                vec![crate::__plugin_logic_deps::BusLayout::stereo()]
535            }
536
537            /// Process one block of audio as a pure function of params
538            /// and input. Same real-time contract as the stateful
539            /// leaf's `process`, minus the state argument.
540            fn process(
541                params: &Self::Params,
542                buffer: &mut crate::__plugin_logic_deps::AudioBuffer<$sample>,
543                events: &crate::__plugin_logic_deps::EventList,
544                context: &mut crate::__plugin_logic_deps::ProcessContext,
545            ) -> crate::__plugin_logic_deps::ProcessStatus;
546
547            /// Translate foreign state into truce shape. Same contract
548            /// as the stateful leaf's `migrate_state` - a stateless
549            /// plugin may still inherit params from a previous
550            /// framework's blob.
551            #[must_use]
552            fn migrate_state(
553                foreign: &crate::__plugin_logic_deps::ForeignState,
554            ) -> Option<crate::__plugin_logic_deps::MigratedState> {
555                let _ = foreign;
556                None
557            }
558
559            /// Construct the editor for this plugin. Required. Same
560            /// contract as the stateful leaf's `editor`.
561            fn editor(
562                params: ::std::sync::Arc<Self::Params>,
563            ) -> Box<dyn crate::__plugin_logic_deps::Editor>;
564        }
565
566        // The blanket that makes the sugar real: a pure plugin IS a
567        // stateful plugin with `DspState = ()`. Everything downstream
568        // (`PluginLogicCore`, `PluginEditor`, the shells, `plugin!`)
569        // binds through $leaf and never learns the difference. Methods
570        // not forwarded here (`init`, `reset`, `save_state`, `latency`,
571        // `tail`, ...) keep their $leaf defaults, which are exactly the
572        // stateless behaviors.
573        impl<T: $name> $leaf for T {
574            type Params = <T as $name>::Params;
575            type DspState = ();
576
577            fn supports_in_place() -> bool {
578                <T as $name>::supports_in_place()
579            }
580
581            fn bus_layouts() -> Vec<crate::__plugin_logic_deps::BusLayout> {
582                <T as $name>::bus_layouts()
583            }
584
585            fn process(
586                _state: &mut (),
587                params: &Self::Params,
588                buffer: &mut crate::__plugin_logic_deps::AudioBuffer<$sample>,
589                events: &crate::__plugin_logic_deps::EventList,
590                context: &mut crate::__plugin_logic_deps::ProcessContext,
591            ) -> crate::__plugin_logic_deps::ProcessStatus {
592                <T as $name>::process(params, buffer, events, context)
593            }
594
595            fn migrate_state(
596                foreign: &crate::__plugin_logic_deps::ForeignState,
597            ) -> Option<crate::__plugin_logic_deps::MigratedState> {
598                <T as $name>::migrate_state(foreign)
599            }
600
601            fn editor(
602                params: ::std::sync::Arc<Self::Params>,
603            ) -> Box<dyn crate::__plugin_logic_deps::Editor> {
604                <T as $name>::editor(params)
605            }
606        }
607    };
608}
609
610pure_plugin_leaf_trait! {
611    /// The stateless `f32` plugin trait: [`PluginLogic`] minus every
612    /// state-shaped item. For a pure parameter-driven effect - one
613    /// whose `process` is a function of params and input only - this
614    /// removes the `type DspState = ()` / `init` / `_state: &mut ()`
615    /// plumbing entirely:
616    ///
617    /// ```ignore
618    /// pub struct Gain;
619    ///
620    /// impl PurePluginLogic for Gain {
621    ///     type Params = GainParams;
622    ///     fn process(params: &GainParams, buffer: &mut AudioBuffer, events: &EventList, ctx: &mut ProcessContext) -> ProcessStatus {
623    ///         /* ... */
624    ///     }
625    ///     fn editor(params: Arc<GainParams>) -> Box<dyn Editor> { /* ... */ }
626    /// }
627    /// ```
628    ///
629    /// A blanket impl makes every `PurePluginLogic` a [`PluginLogic`] with
630    /// `DspState = ()`, so `truce::plugin!` and every shell consume it
631    /// unchanged - and implementing both traits for one type is
632    /// correctly rejected as conflicting. When the plugin grows DSP
633    /// state, switch the impl header to [`PluginLogic`] and add the
634    /// state type and arguments.
635    ///
636    /// Required: [`Self::process`], [`Self::editor`]. Optional:
637    /// [`Self::bus_layouts`], [`Self::supports_in_place`],
638    /// [`Self::migrate_state`]. Anything state-shaped (`reset`,
639    /// `save_state`, `latency`, `tail`, ...) needs state to act on -
640    /// implement [`PluginLogic`] directly if you need those.
641    pub trait PurePluginLogic: PluginLogic<sample = f32>
642}
643
644pure_plugin_leaf_trait! {
645    /// The stateless `f64` plugin trait. Same surface as
646    /// [`PurePluginLogic`] but with the audio buffer pinned to `f64`;
647    /// blanket-implements [`PluginLogic64`]. `truce::prelude64`
648    /// re-exports it as `PurePluginLogic`, so the impl header reads the
649    /// same regardless of precision.
650    pub trait PurePluginLogic64: PluginLogic64<sample = f64>
651}
652
653// ---------------------------------------------------------------------------
654// Bridges - each leaf forwards every method to PluginLogicCore<S>
655// ---------------------------------------------------------------------------
656
657/// Define a blanket `impl<T: $leaf> PluginLogicCore<$sample> for T`
658/// that forwards every trait method to `<T as $leaf>::method(...)`.
659/// One source-of-truth for both `(PluginLogic, f32)` and
660/// `(PluginLogic64, f64)` bridges.
661macro_rules! plugin_logic_bridge {
662    ($leaf:ident, $sample:ty) => {
663        impl<T: $leaf> PluginLogicCore<$sample> for T {
664            type Params = <T as $leaf>::Params;
665            type DspState = <T as $leaf>::DspState;
666
667            const PRESERVE_DSP_STATE: bool = <T as $leaf>::PRESERVE_DSP_STATE;
668
669            fn supports_in_place() -> bool {
670                <Self as $leaf>::supports_in_place()
671            }
672
673            fn bus_layouts() -> Vec<BusLayout> {
674                <Self as $leaf>::bus_layouts()
675            }
676
677            fn init(params: &Self::Params) -> Self::DspState {
678                <Self as $leaf>::init(params)
679            }
680
681            fn reset(state: &mut Self::DspState, params: &Self::Params, config: &AudioConfig) {
682                <Self as $leaf>::reset(state, params, config);
683            }
684
685            fn process(
686                state: &mut Self::DspState,
687                params: &Self::Params,
688                buffer: &mut AudioBuffer<$sample>,
689                events: &EventList,
690                context: &mut ProcessContext,
691            ) -> ProcessStatus {
692                // FTZ/DAZ (or FZ on AArch64) for the duration of
693                // the user's process body. Denormals on filter
694                // feedback paths stall the core; the guard pays
695                // ~two MXCSR writes per block to avoid that. Both the
696                // static and hot shells route process through here, so
697                // this brackets exactly the user body in both modes.
698                let _denormal_guard = DenormalGuard::new();
699                <Self as $leaf>::process(state, params, buffer, events, context)
700            }
701
702            fn save_state(state: &Self::DspState) -> Vec<u8> {
703                <Self as $leaf>::save_state(state)
704            }
705
706            fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool {
707                <Self as $leaf>::snapshot_into(state, buf)
708            }
709
710            fn load_state(state: &mut Self::DspState, data: &[u8]) -> Result<(), StateLoadError> {
711                <Self as $leaf>::load_state(state, data)
712            }
713
714            fn state_changed(state: &mut Self::DspState, params: &Self::Params) {
715                <Self as $leaf>::state_changed(state, params);
716            }
717
718            fn migrate_state(foreign: &ForeignState) -> Option<MigratedState> {
719                <Self as $leaf>::migrate_state(foreign)
720            }
721
722            fn latency(state: &Self::DspState) -> u32 {
723                <Self as $leaf>::latency(state)
724            }
725
726            fn tail(state: &Self::DspState) -> u32 {
727                <Self as $leaf>::tail(state)
728            }
729        }
730
731        impl<T: $leaf> PluginEditor<$sample> for T {
732            type Params = <T as $leaf>::Params;
733
734            fn editor(params: std::sync::Arc<Self::Params>) -> Box<dyn Editor> {
735                <Self as $leaf>::editor(params)
736            }
737        }
738    };
739}
740
741plugin_logic_bridge!(PluginLogic, f32);
742plugin_logic_bridge!(PluginLogic64, f64);
743
744// ---------------------------------------------------------------------------
745// Default hit test - referenced by leaf macro expansions
746// ---------------------------------------------------------------------------
747
748/// Default hit test: circular for knobs, rectangular for everything
749/// else, skip meters. Used by the leaf traits' `hit_test` defaults.
750#[must_use]
751pub fn default_hit_test(widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
752    for (i, w) in widgets.iter().enumerate() {
753        if w.widget_type == WidgetType::Meter {
754            continue;
755        }
756        if w.widget_type == WidgetType::Knob {
757            let dx = x - w.cx;
758            let dy = y - w.cy;
759            if dx * dx + dy * dy <= w.radius * w.radius {
760                return Some(i);
761            }
762        } else if x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h {
763            return Some(i);
764        }
765    }
766    None
767}