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