The Audio-Thread Contract
A dsp block runs on the audio thread. That thread has to hand the sound card a buffer every few
milliseconds, and a late buffer is a click that everyone hears. It may therefore never wait, never
allocate, never touch the disk, and never do an unbounded amount of work. The language of a dsp
block is built so that the code you write in it keeps those rules without your having to think about
them.
The block is compiled, not interpreted
Section titled “The block is compiled, not interpreted”When you run a dsp effect definition, resonon compiles it into the audio graph. Nothing of the
interpreter — its values, its garbage, its locks — reaches the audio thread. Helpers, dsp fns and
object methods are inlined when the block is compiled, so splitting code into functions costs
nothing per sample.
It is also why a dsp block is its own small language rather than all of resonon: only what can run
in bounded time without allocating is in it.
One sample at a time
Section titled “One sample at a time”process and render run once per sample. Every value in the block may change on every sample,
and modulation reaches a parameter per sample too. A block can hold a feedback path — a filter, a
delay with feedback, two oscillators modulating each other — and it is exact to the sample, because
nothing is computed a block ahead.
State is declared up front
Section titled “State is declared up front”Everything a block remembers between samples is declared: state for numbers, buffer for arrays.
The engine knows the whole memory of a block before the first sample, allocates it once, off the
audio thread, and never again. A buffer has a fixed size for the same reason — a delay line that
grew while playing would have to allocate. Its index wraps around, so a buffer is a circular delay
line with no bounds to check.
Every if has an else
Section titled “Every if has an else”In a dsp block, an if without else is an error. Every sample must produce a value on every
path; a branch that could leave a variable unset for one sample would leave a hole in the audio.
The same rule makes if usable as an expression: let gain = if level > t { t / level } else { 1.0 };.
A parameter steps unless told to glide
Section titled “A parameter steps unless told to glide”A value set on a param applies at once. For many parameters that is what you want; for a gain or
a cutoff, a jump from one value to another is a step in the signal, heard as a click or a zipper.
Adding smooth ms to a param makes set values glide there over ms milliseconds. It is a choice
per parameter, not a blanket rule, because what is right for a gain is wrong for a step count.
A signal bound to a parameter is never smoothed: it already moves per sample, and smoothing it would change its shape.
Latency
Section titled “Latency”Some processing can only react to a sample after it has seen later ones: a lookahead limiter delays its output so its gain is down before a peak comes out. That delay has to be the same everywhere in the mix, or the tracks drift apart. The engine lines them up — it delays the other paths by what each one adds — but only for delays it knows about.
The engine knows the delay of the graph it builds from your block. It cannot know the delay your
code creates by reading a buffer late, because that is a fact about your algorithm, not about the
graph. So you declare it: latency: 512; adds 512 samples to what the engine computes for the
block. It is a fixed number because the engine’s compensation is planned for the whole graph at
once, not re-planned sample by sample.
Related
Section titled “Related”- DSP Blocks — every declaration
- Declare Latency — latency in practice
- Recording & Latency — compensation across the whole graph