The effect framework
crates/engine/src/fx/mod.rs defines what every built-in effect implements:
Effect:set_param,param,process(l, r, key, ctx)(in place, at mostMAX_BLOCKframes,keybeing the side-chain signal orNone;ctxhas the sample rate, tempo, whether the transport plays and the song position in beats, which tempo-synced LFOs lock to),reset, and optionallylatency(lookahead, for PDC),tail_secs(for freeze and render tails),metersandset_offline(an offline render is running, soprocessmay block). Every buffer is allocated in the constructor;process,set_paramandresetmust not allocate, lock or block.EffectInfo: the registry entry: the plug-in id (builtin:compressor, …), name, category,ParamSpecs, meter names, whether it takes a side-chain, and a constructor.fx::registry()lists them all,fx::find(id)looks one up,fx::factory_presets(id)returns itsPRESETS.- Parameters are in plain units (dB, ms, Hz, ratio, 0..1 for percentages and toggles, a choice index). Ids are stable forever, because projects and automation lanes store them.
- Shared building blocks:
fx/biquad.rs(an f64 RBJ biquad),fx/dyn_common.rs(smoothing, gain computers, side-chain filter, saturators, true-peak estimation for the Compressor and Limiter), the delay line and note values infx/delay/primitives.rs, which the Reverb and the modulation effects reuse,fx/lfo.rs(LFO, smoother, feedback guard) andfx/oversample.rs(4× half-band oversampling for the distortion effects, with a fixed latency they report). - Linear Phase EQ (
fx/linear_phase_eq.rs,fx/fir.rs): the Channel EQ’s bands (eq::coefficients,EqCoeffs::magnitude) realized as a linear-phase FIR designed by frequency sampling (magnitude on a grid 4× the kernel, zero phase, inverse FFT, Blackman window, odd length so the delay is a whole(taps - 1) / 2frames) and run by uniformly partitioned FFT convolution with a 128-tap direct head, so it adds no latency of its own. Each instance has a worker thread: parameter changes reach it through atomics, it designs at most every 25 ms and passesKernels overrtrbqueues (used ones go back to it to be freed); theConvolvercrossfades old and new kernel outputs over 20 ms, sharing the input spectra. Offline it waits for the worker instead. The panel reuses the Channel EQ editor (EqEditorinEq.tsx) throughsrc/linearPhaseEq.ts.
To add an effect: write fx/<name>.rs with pub fn info() -> EffectInfo and
pub const PRESETS, list it in registry() and factory_presets(), and optionally add a panel
in src/components/fx/ registered in FX_PANELS (src/fx.ts); without one, FxPanel.tsx
shows a generic panel. fx_check covers new registry entries without changes.
In the engine
Section titled “In the engine”dsp/builtin.rs: Plugin::Builtin(Box<BuiltinDsp>) sits next to Plugin::Vst and
Plugin::Instrument in a slot, so bypass, Low Latency Mode, contain() and reordering treat
it like any plug-in.
BuiltinDspclamps values to their spec, and converts automation’s normalized 0..1 values withto_plain/to_normalized(logarithmic forlogspecs, whole steps for toggles and choices).src/fxFormat.tsmirrors the mapping for the panels.- After a
reset, it isfreshuntil it processes: automation applied then snaps instead of gliding, so a render’s first block already has the automated value. FxShared(behind anArc, shared with the session) holds atomics: meters and the values the effect runs with, copied after each block only while the panel iswatched, and the latency and tail, republished on every parameter change. A dynamics effect (one with a meter named “Gain Reduction” or “Reduction”) also keeps its largest gain reduction since the last tick, panel or not, for the strip’s gain-reduction meter (take_gain_reduction).Cmd::FxParamsets a value;Cmd::Sidechainsets the key source (InsertDsp::key, for a VST3 insert too), andresolve_keyskeeps itsidx(the source’s index in the track list) current.Cmd::SidechainPre(PluginSlot::sidechain_pre) makes the key pre-fader: the source keeps a second buffer (keyed_pre,pre_key_l/pre_key_r) copied at the same point as the post-fader key but gated only by mute and solo, not scaled by the fader and pan. Both taps are before the source’s compensation delay, so the key path’s delay compensation doesn’t change.
On the session side
Section titled “On the session side”session/builtin.rs:
- A slot’s values live in the project (
PluginSlot::params, plain units; missing ids are defaults) and its key source inPluginSlot::sidechain, so saving, undo, duplicating, freezing and copy and paste carry them with no plug-in state blob. - Edits go out as
Cmd::FxParam; moving a control while writing automation records a pass as for VST3 parameters (auto_write.rs). - Latency is re-read after every parameter change and polled from
idle, and feedsupdate_pdc. - Side-chain:
Project::process_ordertreats a key like a send (the source plays first), loops are refused, andkey_sourcesadds the sources to an isolated render, where they play but aren’t heard (Role::Key). list_builtin_effectsdescribes the registry to the UI (BuiltinInfo,FxInfoinsrc/fx.ts).watch_builtin(uid, on)is called when a panel opens or closes, and the tick’sfxlist carriesFxTick { uid, meters, values }for watched instances only.
Presets
Section titled “Presets”presets.rs stores them; session/presets.rs applies them.
- Factory presets ship in the code: each effect’s
PRESETS, andeq_factory()for the Channel EQ (preset idbuiltin:channel-eq). A “Default” preset (“Flat” for the EQ) comes first. - User presets are global, not part of a project:
<config dir>/presets/<effect>/<name>.json, with characters that file systems refuse replaced in the effect id (builtin_deesser). Each file is aPreset { name, effect, version, values | eq }, written to a temporary file and renamed into place.clean_nametrims names and limits them to 64 characters. - Commands:
list_presets,get_preset,load_preset(one undo step),save_preset,rename_preset,delete_preset. The Channel EQ is addressed as slotEQ_SLOT(usize::MAX - 1). The pop-up issrc/components/fx/Presets.tsx.