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, cx: &InitContext) -> 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(
253 params: &Self::Params,
254 cx: &$crate::__plugin_logic_deps::InitContext,
255 ) -> Self::DspState {
256 let _ = (params, cx);
257 ::core::default::Default::default()
258 }
259
260 /// Reset for a new sample rate / block size / processing
261 /// mode. Clear `state`'s filters / delay lines; read
262 /// `config.process_mode` to size buffers for an offline
263 /// render (allocation belongs here, off the audio thread) -
264 /// see [`AudioConfig`](truce_core::config::AudioConfig).
265 ///
266 /// Params plumbing is NOT your job: the shell calls
267 /// `params.set_sample_rate(config.sample_rate)` and
268 /// `params.snap_smoothers()` before invoking this, so the
269 /// body only handles state the plugin itself owns. Default:
270 /// no-op, right for a stateless plugin (`DspState = ()`).
271 fn reset(
272 state: &mut Self::DspState,
273 params: &Self::Params,
274 config: &$crate::__plugin_logic_deps::AudioConfig,
275 ) {
276 let _ = (state, params, config);
277 }
278
279 /// Process one block of audio. Real-time - no allocations,
280 /// locks, or I/O. `state` is exclusively owned this block;
281 /// `params` is shared and immutable.
282 fn process(
283 state: &mut Self::DspState,
284 params: &Self::Params,
285 buffer: &mut $crate::__plugin_logic_deps::AudioBuffer<$sample>,
286 events: &$crate::__plugin_logic_deps::EventList,
287 context: &mut $crate::__plugin_logic_deps::ProcessContext,
288 ) -> $crate::__plugin_logic_deps::ProcessStatus;
289
290 /// Serialize plugin-specific state (DSP state, not params -
291 /// those are saved automatically). Default: delegates to
292 /// [`Self::snapshot_into`] (empty when neither is
293 /// overridden).
294 ///
295 /// Runs on a host or GUI thread while the audio thread is
296 /// paused at a block boundary (the wrapper's plugin lock),
297 /// so reading any field is safe - but an audio block that
298 /// arrives mid-save waits for this to return. Keep it
299 /// cheap: copy bytes out, don't compute or compress here.
300 /// To take this off the plugin lock entirely, override
301 /// [`Self::snapshot_into`] instead.
302 fn save_state(state: &Self::DspState) -> Vec<u8> {
303 let mut buf = Vec::new();
304 let _ = Self::snapshot_into(state, &mut buf);
305 buf
306 }
307
308 /// Opt into lock-free state save. `buf` arrives **cleared**,
309 /// with its capacity retained across calls so a steady state
310 /// is allocation-free; fill it with the same bytes
311 /// [`Self::save_state`] would produce (append freely - it is
312 /// never carried over from the previous block).
313 ///
314 /// The return value is a *static capability*, not a
315 /// per-block flag: `true` means "this plugin publishes
316 /// snapshots", `false` means "it never does" (the default).
317 /// Once you return `true` you must return `true` for the
318 /// plugin's whole lifetime - if the custom state empties out,
319 /// return `true` with `buf` left empty (an empty blob), don't
320 /// return `false`. The shell latches the opt-in on the first
321 /// published block; a later `false` is a contract violation
322 /// that would otherwise leave the host reading a stale
323 /// snapshot forever.
324 ///
325 /// Called on the **audio thread** after each process block,
326 /// under the same real-time rules as `process` - bounded, no
327 /// unbounded allocation. The wrapper publishes the result
328 /// into a lock-free slot the host reads without ever taking
329 /// the plugin lock, so saving state while audio runs never
330 /// stalls the audio thread. Overriding this is the
331 /// preferred way to serialize custom state; the default
332 /// [`Self::save_state`] delegates here.
333 fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool {
334 let _ = (state, buf);
335 false
336 }
337
338 /// Restore plugin-specific state into `state`.
339 ///
340 /// Runs on the audio thread between blocks, with the same
341 /// exclusive access `process()` has - writing any field
342 /// is safe.
343 ///
344 /// # Errors
345 ///
346 /// Return `Err(StateLoadError)` when the blob is malformed
347 /// or otherwise can't be interpreted - the format wrapper
348 /// logs the failure (and on hosts that support it, surfaces
349 /// it to the DAW).
350 fn load_state(
351 state: &mut Self::DspState,
352 data: &[u8],
353 ) -> Result<(), $crate::__plugin_logic_deps::StateLoadError> {
354 let _ = (state, data);
355 Ok(())
356 }
357
358 /// Called on the audio thread immediately after
359 /// [`Self::load_state`] returns. Invalidate or recompute any
360 /// caches in `state` that the next `process()` reads. Default:
361 /// no-op.
362 fn state_changed(state: &mut Self::DspState, params: &Self::Params) {
363 let _ = (state, params);
364 }
365
366 /// Translate foreign state - a previous framework's blob,
367 /// or a truce envelope saved under a different plugin id -
368 /// into truce params + extra, so a plugin ported to truce
369 /// keeps its users' old sessions and presets. Runs on the
370 /// host thread; receiverless so it can't touch (or alias)
371 /// the live instance. Return `None` for bytes you don't
372 /// recognize - the wrapper then reports load failure to
373 /// the host, exactly as if this hook didn't exist.
374 ///
375 /// One-shot by construction: the next save writes a normal
376 /// truce envelope, so this never becomes a permanent
377 /// dual-format reader. Keyed formats (AU / LV2 / AAX) only
378 /// see foreign bytes when `truce.toml` declares the legacy
379 /// keys to probe (`[plugin.legacy_state]`).
380 #[must_use]
381 fn migrate_state(
382 foreign: &$crate::__plugin_logic_deps::ForeignState,
383 ) -> Option<$crate::__plugin_logic_deps::MigratedState> {
384 let _ = foreign;
385 None
386 }
387
388 /// Report latency in samples for plugin delay compensation.
389 /// May change at runtime - return a new value and the host is
390 /// notified (see the wrapper latency-change path).
391 fn latency(state: &Self::DspState) -> u32 {
392 let _ = state;
393 0
394 }
395
396 /// Report tail time in samples (audio produced after input
397 /// stops - reverbs, delays). `u32::MAX` for infinite tail.
398 fn tail(state: &Self::DspState) -> u32 {
399 let _ = state;
400 0
401 }
402
403 // ---- GUI ----
404
405 /// Construct the editor for this plugin. Required.
406 ///
407 /// There is no auto-fallback - every plugin explicitly
408 /// names which renderer it wants. For the built-in
409 /// widget layout, call
410 /// `truce_gui::default_editor(params, layout)`; for
411 /// custom renderers, construct an `EguiEditor` /
412 /// `IcedEditor` / `SlintEditor` / hand-rolled `Editor`
413 /// here. The choice of renderer crate the plugin's
414 /// `Cargo.toml` pulls IS the choice of editor.
415 ///
416 /// An associated function, not a method: it receives the
417 /// lock-free `Arc<Self::Params>` store the wrapper already
418 /// holds, so the host can open the editor while audio is
419 /// running without ever taking the plugin lock. Editors bind
420 /// only to the param store (plus meters / transport, all
421 /// lock-free); custom DSP state is read at runtime through
422 /// the editor bridge, not at construction.
423 fn editor(
424 params: ::std::sync::Arc<Self::Params>,
425 ) -> Box<dyn $crate::__plugin_logic_deps::Editor>;
426 }
427 };
428}
429
430// Re-export the dependencies the leaf-trait macro substitutes by path,
431// under one `pub` doc-hidden module so user crates that invoke the
432// macro don't need to import each truce-core type by hand.
433#[doc(hidden)]
434pub mod __plugin_logic_deps {
435 pub use truce_core::buffer::AudioBuffer;
436 pub use truce_core::bus::BusLayout;
437 pub use truce_core::config::AudioConfig;
438 pub use truce_core::dsp_state::{NO_PRESERVE, layout_fingerprint};
439 pub use truce_core::editor::Editor;
440 pub use truce_core::events::EventList;
441 pub use truce_core::process::{ProcessContext, ProcessStatus};
442 pub use truce_core::state::{ForeignState, MigratedState, StateLoadError};
443 pub use truce_core::tasks::{InitContext, TaskSpawner};
444 pub use truce_params::Params;
445}
446
447plugin_logic_leaf_trait! {
448 /// The `f32`-buffer user-facing plugin trait.
449 ///
450 /// Plugin authors implement this in a single `impl` block when
451 /// their audio path is `f32` end-to-end (the default - matches
452 /// the host wire format for nearly all DAWs and formats).
453 /// `truce::prelude` and `truce::prelude32` re-export this name
454 /// directly; `truce::prelude64m` does too (the `m` mixed-precision
455 /// prelude keeps the audio buffer at `f32` and only switches the
456 /// `param.read()` precision).
457 ///
458 /// Required: [`Self::process`], [`Self::editor`]. Everything else
459 /// has a default: `init` builds `Self::DspState::default()` unless
460 /// construction needs params, and `reset` is a no-op. The editor is
461 /// constructed explicitly - layout-only plugins typically call
462 /// `truce_gui::default_editor(params, layout())` (where `layout()`
463 /// is a plain inherent method on the plugin struct, not part of the
464 /// trait). A plugin with no DSP state at all should implement
465 /// [`PurePluginLogic`] instead and skip the state plumbing entirely.
466 ///
467 /// ## Params vs. DSP state
468 ///
469 /// The type you implement this on is a stateless descriptor; the
470 /// data lives in two places, and the method signatures reflect the
471 /// split:
472 ///
473 /// - **Params** (`type Params`) - the user-facing values in your
474 /// `#[derive(Params)]` struct, held as `Arc<Self::Params>`.
475 /// Atomic-backed and `Sync`, shared lock-free with the host and
476 /// the editor. Arrives read-only as `&Self::Params`.
477 /// - **DSP state** (`type DspState`) - filter memory, phase
478 /// accumulators, voice buffers, delay lines. Plain and
479 /// non-atomic, mutated every sample, exclusive to the audio
480 /// thread. Owned by the shell, passed `&mut` to the methods that
481 /// mutate it (`process` / `reset` / `load_state`) and `&` to the
482 /// ones that read it (`save_state` / `snapshot_into`).
483 ///
484 /// `editor` takes neither - it is an associated function over the
485 /// param store, because a GUI is a *view* that binds only params
486 /// (plus lock-free meters / transport) and never touches DSP state,
487 /// so it can be built without the plugin lock. DSP state can't move
488 /// into params: making per-sample filter memory atomic-shared would
489 /// put a synchronized access on the hottest path, and it isn't a
490 /// "parameter" anyway.
491 pub trait PluginLogic<sample = f32>
492}
493
494plugin_logic_leaf_trait! {
495 /// The `f64`-buffer user-facing plugin trait. Same surface as
496 /// [`PluginLogic`] but with the audio buffer pinned to `f64`.
497 ///
498 /// Plugin authors don't usually name this directly - `truce::prelude64`
499 /// re-exports it as `PluginLogic`, so the impl header reads the
500 /// same regardless of which precision the prelude chose. Pick
501 /// `truce::prelude64` (and thus this leaf) when the DSP path runs
502 /// in `f64` end-to-end and the wrapper-boundary widen/narrow
503 /// memcpy is worth the cleaner DSP code.
504 pub trait PluginLogic64<sample = f64>
505}
506
507// ---------------------------------------------------------------------------
508// Pure leaf traits - stateless sugar over PluginLogic / PluginLogic64
509// ---------------------------------------------------------------------------
510
511/// Define a sample-pinned pure (stateless) leaf trait plus its blanket
512/// impl into the matching stateful leaf. Two invocations: `PurePluginLogic`
513/// over [`PluginLogic`] and [`PurePluginLogic64`] over [`PluginLogic64`].
514/// A macro for the same reason as [`plugin_logic_leaf_trait!`]: the two
515/// surfaces stay in lock-step by construction.
516macro_rules! pure_plugin_leaf_trait {
517 ($(#[$attr:meta])* $vis:vis trait $name:ident: $leaf:ident<sample = $sample:ty>) => {
518 $(#[$attr])*
519 $vis trait $name: 'static {
520 /// The plugin's parameter struct (`#[derive(Params)]`).
521 /// Shared, immutable during a block - it arrives by
522 /// reference every call from the shell, which owns the
523 /// `Arc`.
524 type Params: crate::__plugin_logic_deps::Params;
525
526 /// Opt into zero-copy in-place I/O. Same contract as the
527 /// stateful leaf's `supports_in_place`.
528 #[must_use]
529 fn supports_in_place() -> bool {
530 false
531 }
532
533 /// Supported audio bus configurations. Same contract as the
534 /// stateful leaf's `bus_layouts`. Default: stereo in,
535 /// stereo out.
536 #[must_use]
537 fn bus_layouts() -> Vec<crate::__plugin_logic_deps::BusLayout> {
538 vec![crate::__plugin_logic_deps::BusLayout::stereo()]
539 }
540
541 /// Process one block of audio as a pure function of params
542 /// and input. Same real-time contract as the stateful
543 /// leaf's `process`, minus the state argument.
544 fn process(
545 params: &Self::Params,
546 buffer: &mut crate::__plugin_logic_deps::AudioBuffer<$sample>,
547 events: &crate::__plugin_logic_deps::EventList,
548 context: &mut crate::__plugin_logic_deps::ProcessContext,
549 ) -> crate::__plugin_logic_deps::ProcessStatus;
550
551 /// Translate foreign state into truce shape. Same contract
552 /// as the stateful leaf's `migrate_state` - a stateless
553 /// plugin may still inherit params from a previous
554 /// framework's blob.
555 #[must_use]
556 fn migrate_state(
557 foreign: &crate::__plugin_logic_deps::ForeignState,
558 ) -> Option<crate::__plugin_logic_deps::MigratedState> {
559 let _ = foreign;
560 None
561 }
562
563 /// Construct the editor for this plugin. Required. Same
564 /// contract as the stateful leaf's `editor`.
565 fn editor(
566 params: ::std::sync::Arc<Self::Params>,
567 ) -> Box<dyn crate::__plugin_logic_deps::Editor>;
568 }
569
570 // The blanket that makes the sugar real: a pure plugin IS a
571 // stateful plugin with `DspState = ()`. Everything downstream
572 // (`PluginLogicCore`, `PluginEditor`, the shells, `plugin!`)
573 // binds through $leaf and never learns the difference. Methods
574 // not forwarded here (`init`, `reset`, `save_state`, `latency`,
575 // `tail`, ...) keep their $leaf defaults, which are exactly the
576 // stateless behaviors.
577 impl<T: $name> $leaf for T {
578 type Params = <T as $name>::Params;
579 type DspState = ();
580
581 fn supports_in_place() -> bool {
582 <T as $name>::supports_in_place()
583 }
584
585 fn bus_layouts() -> Vec<crate::__plugin_logic_deps::BusLayout> {
586 <T as $name>::bus_layouts()
587 }
588
589 fn process(
590 _state: &mut (),
591 params: &Self::Params,
592 buffer: &mut crate::__plugin_logic_deps::AudioBuffer<$sample>,
593 events: &crate::__plugin_logic_deps::EventList,
594 context: &mut crate::__plugin_logic_deps::ProcessContext,
595 ) -> crate::__plugin_logic_deps::ProcessStatus {
596 <T as $name>::process(params, buffer, events, context)
597 }
598
599 fn migrate_state(
600 foreign: &crate::__plugin_logic_deps::ForeignState,
601 ) -> Option<crate::__plugin_logic_deps::MigratedState> {
602 <T as $name>::migrate_state(foreign)
603 }
604
605 fn editor(
606 params: ::std::sync::Arc<Self::Params>,
607 ) -> Box<dyn crate::__plugin_logic_deps::Editor> {
608 <T as $name>::editor(params)
609 }
610 }
611 };
612}
613
614pure_plugin_leaf_trait! {
615 /// The stateless `f32` plugin trait: [`PluginLogic`] minus every
616 /// state-shaped item. For a pure parameter-driven effect - one
617 /// whose `process` is a function of params and input only - this
618 /// removes the `type DspState = ()` / `init` / `_state: &mut ()`
619 /// plumbing entirely:
620 ///
621 /// ```ignore
622 /// pub struct Gain;
623 ///
624 /// impl PurePluginLogic for Gain {
625 /// type Params = GainParams;
626 /// fn process(params: &GainParams, buffer: &mut AudioBuffer, events: &EventList, ctx: &mut ProcessContext) -> ProcessStatus {
627 /// /* ... */
628 /// }
629 /// fn editor(params: Arc<GainParams>) -> Box<dyn Editor> { /* ... */ }
630 /// }
631 /// ```
632 ///
633 /// A blanket impl makes every `PurePluginLogic` a [`PluginLogic`] with
634 /// `DspState = ()`, so `truce::plugin!` and every shell consume it
635 /// unchanged - and implementing both traits for one type is
636 /// correctly rejected as conflicting. When the plugin grows DSP
637 /// state, switch the impl header to [`PluginLogic`] and add the
638 /// state type and arguments.
639 ///
640 /// Required: [`Self::process`], [`Self::editor`]. Optional:
641 /// [`Self::bus_layouts`], [`Self::supports_in_place`],
642 /// [`Self::migrate_state`]. Anything state-shaped (`reset`,
643 /// `save_state`, `latency`, `tail`, ...) needs state to act on -
644 /// implement [`PluginLogic`] directly if you need those.
645 pub trait PurePluginLogic: PluginLogic<sample = f32>
646}
647
648pure_plugin_leaf_trait! {
649 /// The stateless `f64` plugin trait. Same surface as
650 /// [`PurePluginLogic`] but with the audio buffer pinned to `f64`;
651 /// blanket-implements [`PluginLogic64`]. `truce::prelude64`
652 /// re-exports it as `PurePluginLogic`, so the impl header reads the
653 /// same regardless of precision.
654 pub trait PurePluginLogic64: PluginLogic64<sample = f64>
655}
656
657// ---------------------------------------------------------------------------
658// Background tasks - opt-in managed off-thread work
659// ---------------------------------------------------------------------------
660
661pub use crate::__plugin_logic_deps::{InitContext, TaskSpawner};
662
663/// Opt-in managed background work. A plugin implements this *in addition
664/// to* its leaf trait to declare a `Send` task type and a handler the
665/// framework runs on a shared background-thread pool, then wires it in
666/// with the `tasks:` key on the `truce::plugin!` macro. Nothing changes
667/// for a plugin that doesn't implement it.
668///
669/// `run_task` runs off the audio thread and reaches shared state through
670/// `params` (its `#[skip]` channels / atomics), exactly like the editor:
671/// it must never touch `DspState`, which is audio-thread-exclusive.
672/// Feedback to the audio thread stays the plugin's job through those
673/// `#[skip]` channels.
674///
675/// Keep handlers short and non-blocking. The pool is shared by every
676/// truce plugin in the host and small (`available_parallelism() - 1`
677/// threads, as few as one), so a handler that blocks on I/O (reading a
678/// sample off disk) or waits on a lock stalls background work for *every
679/// other instance too*, not just its own. Allocation and CPU-bound bursts
680/// are fine - that is what the pool is for. For work that genuinely blocks
681/// or runs long, give the plugin its own thread with
682/// `AudioTap::spawn_worker` rather than the shared pool.
683///
684/// Schedule tasks with `ctx.tasks::<Task>()` from `process` (wait-free),
685/// the editor's `PluginContext`, or the `InitContext` passed to `init`.
686///
687/// ```ignore
688/// impl BackgroundTasks for Reverb {
689/// type Params = ReverbParams;
690/// type Task = Rebuild;
691/// fn run_task(task: Rebuild, params: &ReverbParams) {
692/// let graph = build_graph(task.sample_rate, task.time_s);
693/// let _ = params.ready.force_push(graph); // #[skip] handoff
694/// }
695/// }
696/// ```
697pub trait BackgroundTasks {
698 /// The plugin's parameter struct; must match the leaf trait's
699 /// `type Params`.
700 type Params: crate::__plugin_logic_deps::Params;
701 /// A unit of off-thread work. `Send` because the pool moves it across
702 /// threads; `'static` because the worker outlives any block.
703 type Task: Send + 'static;
704 /// Run one task on the pool. See the trait docs for the contract.
705 fn run_task(task: Self::Task, params: &Self::Params);
706}
707
708// ---------------------------------------------------------------------------
709// Bridges - each leaf forwards every method to PluginLogicCore<S>
710// ---------------------------------------------------------------------------
711
712/// Define a blanket `impl<T: $leaf> PluginLogicCore<$sample> for T`
713/// that forwards every trait method to `<T as $leaf>::method(...)`.
714/// One source-of-truth for both `(PluginLogic, f32)` and
715/// `(PluginLogic64, f64)` bridges.
716macro_rules! plugin_logic_bridge {
717 ($leaf:ident, $sample:ty) => {
718 impl<T: $leaf> PluginLogicCore<$sample> for T {
719 type Params = <T as $leaf>::Params;
720 type DspState = <T as $leaf>::DspState;
721
722 const PRESERVE_DSP_STATE: bool = <T as $leaf>::PRESERVE_DSP_STATE;
723
724 fn supports_in_place() -> bool {
725 <Self as $leaf>::supports_in_place()
726 }
727
728 fn bus_layouts() -> Vec<BusLayout> {
729 <Self as $leaf>::bus_layouts()
730 }
731
732 fn init(
733 params: &Self::Params,
734 cx: &crate::__plugin_logic_deps::InitContext,
735 ) -> Self::DspState {
736 <Self as $leaf>::init(params, cx)
737 }
738
739 fn reset(state: &mut Self::DspState, params: &Self::Params, config: &AudioConfig) {
740 <Self as $leaf>::reset(state, params, config);
741 }
742
743 fn process(
744 state: &mut Self::DspState,
745 params: &Self::Params,
746 buffer: &mut AudioBuffer<$sample>,
747 events: &EventList,
748 context: &mut ProcessContext,
749 ) -> ProcessStatus {
750 // FTZ/DAZ (or FZ on AArch64) for the duration of
751 // the user's process body. Denormals on filter
752 // feedback paths stall the core; the guard pays
753 // ~two MXCSR writes per block to avoid that. Both the
754 // static and hot shells route process through here, so
755 // this brackets exactly the user body in both modes.
756 let _denormal_guard = DenormalGuard::new();
757 <Self as $leaf>::process(state, params, buffer, events, context)
758 }
759
760 fn save_state(state: &Self::DspState) -> Vec<u8> {
761 <Self as $leaf>::save_state(state)
762 }
763
764 fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool {
765 <Self as $leaf>::snapshot_into(state, buf)
766 }
767
768 fn load_state(state: &mut Self::DspState, data: &[u8]) -> Result<(), StateLoadError> {
769 <Self as $leaf>::load_state(state, data)
770 }
771
772 fn state_changed(state: &mut Self::DspState, params: &Self::Params) {
773 <Self as $leaf>::state_changed(state, params);
774 }
775
776 fn migrate_state(foreign: &ForeignState) -> Option<MigratedState> {
777 <Self as $leaf>::migrate_state(foreign)
778 }
779
780 fn latency(state: &Self::DspState) -> u32 {
781 <Self as $leaf>::latency(state)
782 }
783
784 fn tail(state: &Self::DspState) -> u32 {
785 <Self as $leaf>::tail(state)
786 }
787 }
788
789 impl<T: $leaf> PluginEditor<$sample> for T {
790 type Params = <T as $leaf>::Params;
791
792 fn editor(params: std::sync::Arc<Self::Params>) -> Box<dyn Editor> {
793 <Self as $leaf>::editor(params)
794 }
795 }
796 };
797}
798
799plugin_logic_bridge!(PluginLogic, f32);
800plugin_logic_bridge!(PluginLogic64, f64);
801
802// ---------------------------------------------------------------------------
803// Default hit test - referenced by leaf macro expansions
804// ---------------------------------------------------------------------------
805
806/// Default hit test: circular for knobs, rectangular for everything
807/// else, skip meters. Used by the leaf traits' `hit_test` defaults.
808#[must_use]
809pub fn default_hit_test(widgets: &[WidgetRegion], x: f32, y: f32) -> Option<usize> {
810 for (i, w) in widgets.iter().enumerate() {
811 if w.widget_type == WidgetType::Meter {
812 continue;
813 }
814 if w.widget_type == WidgetType::Knob {
815 let dx = x - w.cx;
816 let dy = y - w.cy;
817 if dx * dx + dy * dy <= w.radius * w.radius {
818 return Some(i);
819 }
820 } else if x >= w.x && x <= w.x + w.w && y >= w.y && y <= w.y + w.h {
821 return Some(i);
822 }
823 }
824 None
825}