Skip to content

Built-in instruments

crates/engine/src/instrument/mod.rs is the instrument counterpart of fx/mod.rs, and shares what it can: ParamSpec, FactoryPreset, FxContext, presets, FxShared, the panels’ host.

  • Instrument: set_param, param, process(l, r, midi, ctx) (overwrites the block, with MIDI sorted by sample offset, and Midi::parse to decode it), all_notes_off (tails ring on; the engine calls it when the transport stops or jumps), reset (everything played is forgotten, MIDI controllers and random generators included, as a new instance would be), and optionally tail_secs and meters. The real-time rules are the effects’: nothing that allocates, locks or blocks in any of these. Parameter ids are stable forever. There is no latency: instruments have none to report.
  • swap_data, also optional: takes over data that isn’t numeric, built off the audio thread, and gives back what it replaced. It arrives by Cmd::InstrumentData and what it replaced leaves as Garbage::InstrumentData, which the session keeps until it holds the last reference, so notes still reading the old data through clones never free it on the audio thread. The settings behind the data live in PluginSlot::state, carried by saving, undo, copying and Freeze, and a Preset has a state field for them. A preset without one leaves what’s there alone.
  • InstrumentInfo: the registry entry: id (builtin:tre), name, menu group, about, params, sections (the panel’s layout), meters, presets and new(sample_rate, seed). seed is the slot’s id, saved in the project, so an instance’s analog differences stay with it across reopening, and a duplicate gets its own.
  • In the engine: Plugin::Instrument(Box<InstrumentDsp>) (dsp/builtin.rs), BuiltinDsp’s sibling. Cmd::FxParam and automation reach it the same way. silence_instruments calls reset around offline renders; Track Off calls it through Plugin::sleep; bypassing one lets go of its notes, since it won’t hear their note-offs.
  • In the session (session/builtin.rs): an instrument slot is addressed as slot usize::MAX (-1 in the UI’s INSTRUMENT_SLOT) in set_builtin_param, load_preset, save_preset and the rest. find_builtin and builtin_spec find instruments and effects alike, so automation, MIDI learn, undo, presets, panels and copy and paste share one path. is_builtin is what keeps a built-in from counting as a missing plug-in. builtin_tail_secs includes the instrument, for Freeze.
  • Multi-core: a track with an instrument counts toward heavy_width, the same whichever kind it holds. An instrument touches nothing but itself, and runs under its strip’s task.
  • In the UI: list_builtin_instruments fills instList (src/store.ts). fxInfo looks through both lists; isBuiltinFx is true for what has parameters, so Marbble Synth still has no panel. InstrumentPanel (src/components/fx/Instrument.tsx) shows the sections and a keyboard, unless FX_PANELS has a custom panel for that id. The keyboard selects the track and plays through midi_event, the Musical Typing path.

Marbble Synth stays as it was: basic.rs wraps BasicSynth without changing it. Old projects render bit for bit as before. It has a quirk the contract forbids for new instruments: silence keeps the mod wheel, expression and vibrato LFO, the LFO running free even while stopped, so with the wheel up its renders differ from one to the next.

Measured by the tests in each file, at 48 kHz unless it says otherwise.

Part What it is Measured
osc::Osc Saw, reverse saw, pulse with PWM, triangle of any symmetry, sine; hard sync. A naive wave with 4-point B-spline polyBLEP and polyBLAMP residuals over a 2-sample delay, so an event the oscillator couldn’t foresee (sync, PWM) corrects the same way. f64 phase. Aliasing in the audio band, against the fundamental, run at 2x through resample: saw -87 dB at 1 kHz, -83 at 2 kHz, -82 at 4 kHz, -87 at 8 kHz. Pulse -90, -87, -82, -105. Triangle -124, -115, -107, -119. A naive saw at 4 kHz is -17 dB; polyBLEP at 1x is -41 dB. Hard sync at 1 kHz, ratio 2.37: -75 dB. Tuning: under 0.001 cents in every test (a note-to-Hz accuracy of 0.002 cents).
osc::Noise White, and pink by Paul Kellet’s filter, from a seed.
filter::Ladder A 4-pole transistor ladder, ZDF, Mystran’s linearized tanh per stage, per-stage tolerances, behind the ZdfFilter trait. Trimmed so self-oscillation stays in tune as it saturates, by a fit to measurements here. Self-oscillation at k = 5, 2x: within 3.4 cents from note 24 to 96 (after the trim; it would be 70 cents flat). 24 dB/octave, -12 dB at the cutoff, flat below. No NaN or runaway at any setting, at 1000 times full scale with the cutoff modulated at the audio rate.
filter::OtaCascade Four OTA integrators and one feedback path (the IR3109, CEM3320 and SSM2040 family): each stage bends on the difference across it, not its level. Two character constants: slew and bass. Used by Sei and Cinque. Self-oscillation at k = 4.3, 2x: within 27 cents from note 36 to 96 (untrimmed, worst at the top). 24 dB/octave, flat below.
filter::DiodeLadder Four coupled rungs, first capacitor 0.545 of the others, a high-pass in the feedback. Solved as a tridiagonal system within the sample. Resonance 1 is the linear circuit’s instability (k = 21.56, at 1.421 / RC, from its polynomial, worked out in a script). Used by Uno, and in Onda’s list. An octave above the cutoff -13.6 dB, the next -18.2, far above -24.1: the “18 dB” feel is real in this model. The pass band sags from a decade below the cutoff. Self-oscillates at 1.05, 59 to 90 cents flat of the cutoff (no trim: nothing here tracks a keyboard).
filter::Svf A 2-pole state-variable (Simper’s trapezoidal form) with low, band and high outputs, and damping that grows with the band-pass level. Used by Onda. 12 dB/octave, unity pass bands, a notch at the cutoff.
env::Envelope, EnvShape RC curves: attack charging toward 1.3 and stopping at 1, decay and release exponential, times to 95 %. Retrigger from the current level. Times within 2 to 3 % from 1 ms to 10 s.
lfo::Lfo Sine, triangle, saw, square, sample-and-hold. Hz, or per beat locked to the song position while playing. Locks on the beat, same values for any block size at a steady tempo.
alloc::VoiceAllocator Poly with stealing (free, then the same note ringing, then longest released, then oldest held). Mono with low, last or high priority, single or multi trigger. Unison in whole groups. The sustain pedal. set_rotate takes free voices in turn, as analog polys’ assigners do. State-machine tests, in the file.
analog Rng, Drift (a slow random walk), Tolerance (one voice’s fixed differences), sub_seed. All from seeds: no global, no clock. The same seed gives the same stream.
resample::Decimator2x An 8-coefficient polyphase allpass half-band, no latency to report (about 2.6 output samples at low frequencies with the oscillator’s delay, 54 µs at 48 kHz). Flat to 0.225 of the high rate within 0.05 dB, stop band -86.7 dB.
smooth Smoothed (a one-pole lag that lands exactly on its target), Glide.

Stealing: the allocator says stolen and the voice decides. A voice with free-running oscillators needs nothing (env.rs retriggers from the current level, and the oscillators’ and filter’s state carry on). A voice whose phase jumps, like a sampler’s, should keep spare voices and fade the stolen one.