Skip to content

DSP Blocks

A dsp block is compiled into the audio graph and runs once per sample. The code its functions may contain is in DSP Code; the functions and constants it can call are in the DSP builtins reference.

DeclarationMakesEntry pointInstantiated with
dsp effect Name { … }an effectfn process(…) -> …Name("slot"), placed with track.set_effects(#[…])
dsp instrument Name { … }an instrumentfn render(note, velocity, gate) -> (l, r)Name(), placed with track.set_instrument(…)
dsp signal Name { … }a signalfn process(…) -> outName(…), bound with <<
dsp fn name(…) -> out { … }a pure function—called from dsp code
dsp object Name { … }a stateful component—let x = Name(); inside dsp code
Membereffectinstrumentsignalobject
param name: default range(min, max) smooth ms;✓✓✓—
state name: initial;✓✓✓✓
voice state name: initial;—✓——
buffer name(size);✓✓✓✓
input name: stereo; / input name: mono;✓———
output: stereo; / output: mono;✓———
latency: samples;✓✓——
range: min..max——✓—
fn name(…) -> out { … }✓✓✓✓
  • param — a control, read by name inside the block and set from outside with .parameter(name), .set_parameter and <<. range(min, max) is optional; it scales normalized values and bounds modulation. smooth ms is optional: a value that is set glides to its new value over ms milliseconds instead of stepping. A bound signal is never smoothed. Without smooth, a set value applies at once. In an instrument, one value is shared by all voices.
  • state — a variable that keeps its value from one sample to the next.
  • voice state — a state of which every voice has its own copy, set back to its initial value on each note-on. In an instrument, a plain state currently behaves the same way.
  • buffer name(size) — size numbers, all 0 at the start. An index is truncated to a whole number and wraps modulo size, so buf[size] is buf[0]; a negative index reads and writes buf[0]. In an instrument, every voice has its own buffers, cleared on each note-on.
  • latency: samples — the delay the block adds, in samples, on top of what the engine computes for its graph. Default 0. See Declare Latency.
  • range: min..max — the span of a signal’s output. Default -1..1. It takes no ;.

The clock constants CPS, BPC, CYCLE, TIME, CYCLES, POSITION, CYCLE_PHASE, BEAT_PHASE, CYCLE_EDGE and BEAT_EDGE, and the MIDI functions cc, pitchbend and aftertouch, follow the session only in a dsp signal — and in the dsp fns and object methods it calls. In an effect or an instrument, TIME, CYCLES, POSITION and the MIDI functions are unknown names, CPS, BPC and CYCLE are the fixed values 1, 4 and 0, and the phase and edge constants are 0. SR, INV_SR, PI and TWOPI work everywhere.

TIME and CYCLES are how far the signal has run since it started, in seconds and in cycles. A tempo change bends CYCLES and never jumps it, and the phase and edge constants are built on it (CYCLE_PHASE is fract(CYCLES)). Use CYCLES for anything that should stay on the beat, and TIME for anything that should keep its speed whatever the tempo.

POSITION is the same distance counted in the signal’s timebase: cycles unless .set_timebase() sets another unit. Use it for any length a user should be able to give in their own unit — the stdlib Ramp measures its duration with it, so signal_ramp(0, 1, 4).set_timebase(seconds(1)) is a four-second ramp.

Without input and output declarations, an effect is stereo in, stereo out:

fn process(left, right) -> (out_l, out_r)

With declarations, process takes one argument per input channel, in declaration order — two for a stereo input, one for a mono one — and returns one value per output channel:

DeclarationsSignature
input main: stereo; input sidechain: mono; output: stereo;fn process(main_l, main_r, sc) -> (out_l, out_r)
input main: mono; output: mono;fn process(input) -> output

Every input after the first is fed with effect.connect_input(name, track).

fn render(note, velocity, gate) -> (out_l, out_r)
ArgumentValue
noteMIDI note number; fractional for pitches between semitones
velocity0.0–1.0
gate1.0 while the note is held, 0.0 after its note-off

render runs once per sample for every sounding voice; the voices are summed. An instrument has 16 voices. A note takes the first free voice; when none is free, the quietest voice is taken. After note-off a voice keeps running until its output stays below −120 dBFS (1e-6) for 64 samples, then it is free.

fn process(input_1, input_2, …) -> out

process returns one value per sample. Its arguments, if any, are inputs fed by other signals.

The constructor takes the inputs first, then the params in declaration order. Any argument can be given by name instead. A number where a signal is expected is a constant signal; a signal given for a param modulates it per sample.

Remap(Wobble()) // input `source` = Wobble()
Remap(source: Wobble(), lo: 0.2) // by name
Wobble(8.0) // no inputs: first param, speed = 8
dsp fn name(a, b) -> out { … }

A pure function: no param, state or buffer, and one return value. Every dsp block of the file can call it, wherever the dsp fn is written.

dsp object Name {
state …;
buffer …;
fn method(…) -> out { … }
}

Methods may have any name; there is no entry point and no param. Each let x = Name(); inside a block is a separate instance with its own state.

Any fn in a block other than its entry point is a helper:

  • It is visible only in its block, and shadows a dsp fn of the same name.
  • It can read and write the block’s param, state, voice state and buffer names, but sees only its own arguments — not the entry point’s arguments or locals.
  • A block declares each helper once, and has one entry point; a second fn process or fn render is an error.

Every helper, dsp fn and dsp object method is checked when it is compiled, called or not.

dsp fn and dsp object are exported from a module like any other definition, unless marked private, and imported with a selective use. Only a module’s dsp blocks can call imported DSP code; the blocks of the file being run, or of an executed selection, can call only what that file defines itself.

/// above a dsp declaration, a param or a helper documents it for the editor: hovering an effect lists its documented params, and hovering a param or helper shows its own comment. A signal’s params are its constructor’s arguments, and are listed as such.

/// A gain stage with a soft ceiling.
dsp effect SoftGain {
/// Linear gain before the ceiling.
param level: 1.0 range(0, 4);
/// Bends `x` smoothly towards ±1.
fn soften(x) -> y {
return __native("tanh", x);
}
fn process(left, right) -> (out_l, out_r) {
return (soften(left * level), soften(right * level));
}
}