Skip to content

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 most MAX_BLOCK frames, key being the side-chain signal or None; ctx has the sample rate, tempo, whether the transport plays and the song position in beats, which tempo-synced LFOs lock to), reset, and optionally latency (lookahead, for PDC), tail_secs (for freeze and render tails), meters and set_offline (an offline render is running, so process may block). Every buffer is allocated in the constructor; process, set_param and reset must 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 its PRESETS.
  • 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 in fx/delay/primitives.rs, which the Reverb and the modulation effects reuse, fx/lfo.rs (LFO, smoother, feedback guard) and fx/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) / 2 frames) 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 passes Kernels over rtrb queues (used ones go back to it to be freed); the Convolver crossfades 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 (EqEditor in Eq.tsx) through src/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.

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.

  • BuiltinDsp clamps values to their spec, and converts automation’s normalized 0..1 values with to_plain / to_normalized (logarithmic for log specs, whole steps for toggles and choices). src/fxFormat.ts mirrors the mapping for the panels.
  • After a reset, it is fresh until it processes: automation applied then snaps instead of gliding, so a render’s first block already has the automated value.
  • FxShared (behind an Arc, shared with the session) holds atomics: meters and the values the effect runs with, copied after each block only while the panel is watched, 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::FxParam sets a value; Cmd::Sidechain sets the key source (InsertDsp::key, for a VST3 insert too), and resolve_keys keeps its idx (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.

session/builtin.rs:

  • A slot’s values live in the project (PluginSlot::params, plain units; missing ids are defaults) and its key source in PluginSlot::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 feeds update_pdc.
  • Side-chain: Project::process_order treats a key like a send (the source plays first), loops are refused, and key_sources adds the sources to an isolated render, where they play but aren’t heard (Role::Key).
  • list_builtin_effects describes the registry to the UI (BuiltinInfo, FxInfo in src/fx.ts). watch_builtin(uid, on) is called when a panel opens or closes, and the tick’s fx list carries FxTick { uid, meters, values } for watched instances only.

presets.rs stores them; session/presets.rs applies them.

  • Factory presets ship in the code: each effect’s PRESETS, and eq_factory() for the Channel EQ (preset id builtin: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 a Preset { name, effect, version, values | eq }, written to a temporary file and renamed into place. clean_name trims 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 slot EQ_SLOT (usize::MAX - 1). The pop-up is src/components/fx/Presets.tsx.