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, andMidi::parseto 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 optionallytail_secsandmeters. 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 byCmd::InstrumentDataand what it replaced leaves asGarbage::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 inPluginSlot::state, carried by saving, undo, copying and Freeze, and aPresethas astatefield for them. A preset without one leaves what’s there alone.InstrumentInfo: the registry entry: id (builtin:tre), name, menugroup,about,params,sections(the panel’s layout),meters,presetsandnew(sample_rate, seed).seedis 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::FxParamand automation reach it the same way.silence_instrumentscallsresetaround offline renders; Track Off calls it throughPlugin::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 slotusize::MAX(-1in the UI’sINSTRUMENT_SLOT) inset_builtin_param,load_preset,save_presetand the rest.find_builtinandbuiltin_specfind instruments and effects alike, so automation, MIDI learn, undo, presets, panels and copy and paste share one path.is_builtinis what keeps a built-in from counting as a missing plug-in.builtin_tail_secsincludes 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_instrumentsfillsinstList(src/store.ts).fxInfolooks through both lists;isBuiltinFxis true for what has parameters, so Marbble Synth still has no panel.InstrumentPanel(src/components/fx/Instrument.tsx) shows the sections and a keyboard, unlessFX_PANELShas a custom panel for that id. The keyboard selects the track and plays throughmidi_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.
Shared DSP (synth_dsp/)
Section titled “Shared DSP (synth_dsp/)”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.