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