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