pub trait PluginLogic: 'static {
type Params: Params;
type DspState: Default + Send + 'static;
const PRESERVE_DSP_STATE: bool = true;
Show 15 methods
// Required methods
fn process(
state: &mut Self::DspState,
params: &Self::Params,
buffer: &mut AudioBuffer<'_, f32>,
events: &EventList,
context: &mut ProcessContext<'_>,
) -> ProcessStatus;
fn editor(params: Arc<Self::Params>) -> Box<dyn Editor>;
// Provided methods
fn supports_in_place() -> bool { ... }
fn bus_layouts() -> Vec<BusLayout> { ... }
fn init(params: &Self::Params, cx: &InitContext) -> Self::DspState { ... }
fn reset(
state: &mut Self::DspState,
params: &Self::Params,
config: &AudioConfig,
) { ... }
fn save_state(state: &Self::DspState) -> Vec<u8> ⓘ { ... }
fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool { ... }
fn snapshot_version(state: &Self::DspState) -> Option<u64> { ... }
fn snapshot_prealloc_hint() -> usize { ... }
fn load_state(
state: &mut Self::DspState,
data: &[u8],
) -> Result<(), StateLoadError> { ... }
fn state_changed(state: &mut Self::DspState, params: &Self::Params) { ... }
fn migrate_state(foreign: &ForeignState<'_>) -> Option<MigratedState> { ... }
fn latency(state: &Self::DspState) -> u32 { ... }
fn tail(state: &Self::DspState) -> u32 { ... }
}Expand description
The f32-buffer user-facing plugin trait.
Plugin authors implement this in a single impl block when
their audio path is f32 end-to-end (the default - matches
the host wire format for nearly all DAWs and formats).
truce::prelude and truce::prelude32 re-export this name
directly; truce::prelude64m does too (the m mixed-precision
prelude keeps the audio buffer at f32 and only switches the
param.read() precision).
Required: Self::process, Self::editor. Everything else
has a default: init builds Self::DspState::default() unless
construction needs params, and reset is a no-op. The editor is
constructed explicitly - layout-only plugins typically call
truce_gui::default_editor(params, layout()) (where layout()
is a plain inherent method on the plugin struct, not part of the
trait). A plugin with no DSP state at all should implement
PurePluginLogic instead and skip the state plumbing entirely.
§Params vs. DSP state
The type you implement this on is a stateless descriptor; the data lives in two places, and the method signatures reflect the split:
- Params (
type Params) - the user-facing values in your#[derive(Params)]struct, held asArc<Self::Params>. Atomic-backed andSync, shared lock-free with the host and the editor. Arrives read-only as&Self::Params. - DSP state (
type DspState) - filter memory, phase accumulators, voice buffers, delay lines. Plain and non-atomic, mutated every sample, exclusive to the audio thread. Owned by the shell, passed&mutto the methods that mutate it (process/reset/load_state) and&to the ones that read it (save_state/snapshot_into).
editor takes neither - it is an associated function over the
param store, because a GUI is a view that binds only params
(plus lock-free meters / transport) and never touches DSP state,
so it can be built without the plugin lock. DSP state can’t move
into params: making per-sample filter memory atomic-shared would
put a synchronized access on the hottest path, and it isn’t a
“parameter” anyway.
Provided Associated Constants§
Sourceconst PRESERVE_DSP_STATE: bool = true
const PRESERVE_DSP_STATE: bool = true
Whether the hot-reload shell carries live DSP state across a
reload. Default true: the shell serializes the state
through the old dylib and restores it into the new one via
save_state / load_state,
so a reverb tail survives an edit-and-reload whenever the
plugin serializes it. Set to false to always re-init on
reload.
Required Associated Types§
Sourcetype Params: Params
type Params: Params
The plugin’s parameter struct (#[derive(Params)]).
Shared, immutable during a block - it arrives by
reference every call from the shell, which owns the
Arc. Never stored in Self::DspState.
Sourcetype DspState: Default + Send + 'static
type DspState: Default + Send + 'static
The mutable per-block audio state - filter memory, voice
buffers, phase accumulators. A plain struct, distinct from
the descriptor Self (except for the small-effect
type DspState = Self shape). A plugin with no audio state
implements the stateless leaf trait instead of this one and
never names a DspState. Owned by the shell, so it can
outlive a code swap.
Default is how the state is born: the default
Self::init returns Self::DspState::default(), so most
plugins never write init - they #[derive(Default)] (or
hand-write Default when a fresh state has non-zero fields)
and override init only when construction needs params.
No layout trait is required - on reload the hot-reload shell
carries the state into the new code by a save / load
round-trip, never by reinterpreting the old bytes.
Required Methods§
Sourcefn process(
state: &mut Self::DspState,
params: &Self::Params,
buffer: &mut AudioBuffer<'_, f32>,
events: &EventList,
context: &mut ProcessContext<'_>,
) -> ProcessStatus
fn process( state: &mut Self::DspState, params: &Self::Params, buffer: &mut AudioBuffer<'_, f32>, events: &EventList, context: &mut ProcessContext<'_>, ) -> ProcessStatus
Process one block of audio. Real-time - no allocations,
locks, or I/O. state is exclusively owned this block;
params is shared and immutable.
Sourcefn editor(params: Arc<Self::Params>) -> Box<dyn Editor>
fn editor(params: Arc<Self::Params>) -> Box<dyn Editor>
Construct the editor for this plugin. Required.
There is no auto-fallback - every plugin explicitly
names which renderer it wants. For the built-in
widget layout, call
truce_gui::default_editor(params, layout); for
custom renderers, construct an EguiEditor /
IcedEditor / SlintEditor / hand-rolled Editor
here. The choice of renderer crate the plugin’s
Cargo.toml pulls IS the choice of editor.
An associated function, not a method: it receives the
lock-free Arc<Self::Params> store the wrapper already
holds, so the host can open the editor while audio is
running without ever taking the plugin lock. Editors bind
only to the param store (plus meters / transport, all
lock-free); custom DSP state is read at runtime through
the editor bridge, not at construction.
Provided Methods§
Sourcefn supports_in_place() -> bool
fn supports_in_place() -> bool
Opt into zero-copy in-place I/O. When this returns true,
the format wrapper skips its safety memcpy on host-aliased
buffers and hands the plugin the raw shared memory through
AudioBuffer::in_out_mut(ch). The plugin must check
AudioBuffer::is_in_place(ch) per channel before reading
input(ch).
Default false: the wrapper copies aliased inputs into
scratch so input(ch) and output(ch) are always
disjoint. Costs one memcpy per aliased channel per block.
Sourcefn bus_layouts() -> Vec<BusLayout>
fn bus_layouts() -> Vec<BusLayout>
Supported audio bus configurations. The host picks one;
the others are rejected at bus-config time before
process is ever called. Default: the standard audio
effect - stereo and mono - so it shows on both track widths.
Sourcefn init(params: &Self::Params, cx: &InitContext) -> Self::DspState
fn init(params: &Self::Params, cx: &InitContext) -> Self::DspState
Build the initial audio state from params. Replaces the
old new constructor: the descriptor is stateless, so
state is born here and owned by the shell. Not real-time
safe - allocate freely.
Default: Self::DspState::default(). Override only when
construction genuinely needs to read params; a fixed
initial state belongs in the state type’s Default impl
instead.
Sourcefn reset(
state: &mut Self::DspState,
params: &Self::Params,
config: &AudioConfig,
)
fn reset( state: &mut Self::DspState, params: &Self::Params, config: &AudioConfig, )
Reset for a new sample rate / block size / processing
mode. Clear state’s filters / delay lines; read
config.process_mode to size buffers for an offline
render (allocation belongs here, off the audio thread) -
see AudioConfig.
Params plumbing is NOT your job: the shell calls
params.set_sample_rate(config.sample_rate) and
params.snap_smoothers() before invoking this, so the
body only handles state the plugin itself owns. Default:
no-op, right for a stateless plugin (DspState = ()).
Sourcefn save_state(state: &Self::DspState) -> Vec<u8> ⓘ
fn save_state(state: &Self::DspState) -> Vec<u8> ⓘ
Serialize plugin-specific state (DSP state, not params - those are saved automatically).
Prefer Self::snapshot_into. That is the
real-time-safe path and the one CLAP / VST3 / AU actually
persist from - Self::snapshot_into’s default delegates
here, so overriding only save_state still round-trips. The
catch: it then runs on the audio thread each changed
block, so keep it cheap (copy bytes out; no compute or
compress) and reach for snapshot_into (or the off-thread
InitContext::snapshot_publisher() for MB-scale state) for
anything non-trivial. Default: empty (no extra state).
Sourcefn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool
fn snapshot_into(state: &Self::DspState, buf: &mut Vec<u8>) -> bool
Opt into lock-free state save. buf arrives cleared,
with its capacity retained across calls so a steady state
is allocation-free; fill it with the same bytes
Self::save_state would produce (append freely - it is
never carried over from the previous block).
The return value is a static capability, not a
per-block flag: true means “this plugin publishes
snapshots”, false means “it never does”. Once you return
true you must return true for the plugin’s whole
lifetime - if the custom state empties out, return true
with buf left empty (an empty blob), don’t return false.
The shell latches the opt-in on the first published block; a
later false is a contract violation that would otherwise
leave the host reading a stale snapshot forever.
Called on the audio thread after each process block
whose Self::snapshot_version changed, under the same
real-time rules as process - bounded, no unbounded
allocation. The wrapper publishes the result into a
lock-free slot the host reads without ever taking the
plugin lock, so saving state while audio runs never stalls
the audio thread.
Default. Delegates to Self::save_state, so a plugin
that overrides only the legacy save_state still publishes
through this slot - at the cost of running save_state on
the audio thread. A plugin overriding neither publishes
nothing (its empty save_state).
Size regime. This inline lane copies the whole buffer
on every changed block, so it is for KB-scale state (a
file path, a view mode, a small analysis buffer). For
MB-scale state (a sampler’s audio, big wavetables) copying
on the audio thread is itself a hazard - publish that off the
audio thread instead via
InitContext::snapshot_publisher() (a background-serialized,
pointer-swapped buffer) and leave this at the default.
Sourcefn snapshot_version(state: &Self::DspState) -> Option<u64>
fn snapshot_version(state: &Self::DspState) -> Option<u64>
Generation token for the state Self::snapshot_into
serializes. Bump it (any monotonic change is enough -
a counter you increment when custom state mutates) so the
shell re-serializes only when it changes: an unchanged
block then pays O(1) - a single integer read - regardless
of snapshot size, instead of re-copying the whole buffer
every block.
Read on the audio thread each block, so keep it trivial
(read a field; no work). None (the default) means “no
version tracking - re-serialize every block”, which is fine
for tiny state but pays the full copy each block for larger
state. Return Some(token) to opt into gating.
Sourcefn snapshot_prealloc_hint() -> usize
fn snapshot_prealloc_hint() -> usize
Bytes to pre-warm the inline snapshot buffer to, off the
audio thread, so the first Self::snapshot_into publish of
up to this many bytes doesn’t reallocate on the audio thread.
Default 256. Raise it to your typical inline snapshot size;
genuinely large state should use the off-thread publisher
instead (which never touches this buffer).
Sourcefn load_state(
state: &mut Self::DspState,
data: &[u8],
) -> Result<(), StateLoadError>
fn load_state( state: &mut Self::DspState, data: &[u8], ) -> Result<(), StateLoadError>
Restore plugin-specific state into state.
Runs on the audio thread between blocks, with the same
exclusive access process() has - writing any field
is safe.
§Errors
Return Err(StateLoadError) when the blob is malformed
or otherwise can’t be interpreted - the format wrapper
logs the failure (and on hosts that support it, surfaces
it to the DAW).
Sourcefn state_changed(state: &mut Self::DspState, params: &Self::Params)
fn state_changed(state: &mut Self::DspState, params: &Self::Params)
Called on the audio thread immediately after
Self::load_state returns. Invalidate or recompute any
caches in state that the next process() reads. Default:
no-op.
Sourcefn migrate_state(foreign: &ForeignState<'_>) -> Option<MigratedState>
fn migrate_state(foreign: &ForeignState<'_>) -> Option<MigratedState>
Translate foreign state - a previous framework’s blob,
or a truce envelope saved under a different plugin id -
into truce params + extra + persist, so a plugin ported
to truce keeps its users’ old sessions and presets. For a
renamed-plugin envelope, forward the decoded persist
bytes to keep #[persist] fields across the rename. Runs
on the host thread; receiverless so it can’t touch (or
alias) the live instance. Return None for bytes you
don’t recognize - the wrapper then reports load failure
to the host, exactly as if this hook didn’t exist.
One-shot by construction: the next save writes a normal
truce envelope, so this never becomes a permanent
dual-format reader. Keyed formats (AU / LV2 / AAX) only
see foreign bytes when truce.toml declares the legacy
keys to probe ([plugin.legacy_state]).
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".