Source
Audio off — tap to enable

An interactive primer · every figure drawn by the code it describes

Signal
Path

A synthesizer is a function that gets handed an empty buffer a few thousand times a second and has to fill it with numbers. Everything else — oscillators, filters, envelopes, the whole vocabulary — is a way of deciding what those numbers should be. This book builds that function one piece at a time, and lets you hear each piece the moment it exists.

Hero · Pure tone

One partial · the smallest sound there is
Freq Level Transport
Drag a knob vertically; hold Shift for fine control, double-click to reset. Nothing plays until you press Play — browsers require a gesture before any audio starts. Every sound on this page is computed sample by sample in an AudioWorklet from the source shown in the code blocks, not from a built-in oscillator node.
Ch 00 sampling · nyquist · quantisation · decibels · pitch

Sound as Numbers

Before any synthesis, one conversion: something you can hear becomes a list of numbers. Every limitation you will fight later is already hiding in that step.

If you have no maths or physics background

This chapter is written so you can start from nothing. Every symbol is explained before it is used, there is a table of notation below, and a glossary at the end. Nothing here requires anything beyond multiplying and dividing — the frightening-looking formulas are all shorthand for pictures, and you will be shown the picture first.

Start with the wiggle

Sound is air being squashed and stretched. A speaker cone pushes forward, crowding the air in front of it; it pulls back, thinning that air out. Do that fast enough and the ripple reaches your eardrum, which wiggles in the same pattern, and you hear something.

Two things about that wiggle matter most:

  • How fast it wiggles decides the pitch. Faster is higher.
  • How far it wiggles decides the loudness. Bigger is louder.

A third thing — the exact shape of the wiggle — decides why a violin and a flute playing the same note still sound like different instruments. That one takes until Chapter 2.

The flipbook

A film is not moving. It is a stack of still photographs shown twenty-four times a second, fast enough that your eye stops noticing the gaps. Digital audio works the same way, only far faster: instead of photographs, we take measurements of the air pressure, about 48,000 times every second.

One measurement is called a sample. That is the whole idea. A sample is not a tiny piece of sound and it has no length — it is one reading, like one frame of the film. String enough of them together and play them back at the same rate, and you have sound.

Fig 0.1 — The grey curve is the real pressure wave, which is smooth and continuous. The orange dots are our measurements — the only thing we actually keep. Playback is the job of drawing a smooth curve back through those dots.

What a sample actually is

Inside a synthesizer there is no microphone and nothing to measure. We invent the numbers directly. That is the entire job: decide what number comes next, 48,000 times a second.

By convention each number runs from -1.0 to +1.0. Zero means the air is undisturbed — silence. Positive means pushed, negative means pulled. +1.0 is as loud as the system can go, which engineers call full scale.

We keep these as decimals rather than whole numbers. The reason is practical: when you add several sounds together the total can briefly overshoot 1.0, and a decimal simply carries the larger value along until a later stage brings it back down. Whole numbers would wrap around and turn the overshoot into a loud crackle. The conversion to whole numbers happens once, at the very end, inside the sound card.

The notation, once, so it stops being scary

Every symbol used in this chapter, in plain words:

SymbolSaid aloudWhat it means
π"pie"A fixed number, about 3.14159. It just is that value, the way 7 is 7.
2π"two pie"One complete turn around a circle. Maths measures angles this way instead of in degrees; 2π means the same thing as 360°.
sin(…)"sine of…"A button on a calculator. Give it an angle, it gives back the height of a point on a circle. The next section shows exactly what that means.
f"eff"Frequency — how many times per second something repeats.
fs"eff ess"Sample rate — how many measurements we take per second. Usually 48,000.
Hz"hertz"Just a unit meaning "times per second". 440 Hz is 440 wiggles per second.
2ⁿ"two to the n"2 multiplied by itself n times. 2³ = 2×2×2 = 8.
log₁₀(x)"log of x"The reverse question: "10 to what power gives me x?" Since 10×10 = 100, log₁₀(100) = 2.
≈"about"Approximately equal.

Where the sine wave comes from

One shape of wiggle matters more than every other, because Chapter 2 will show that all the others are built out of it. It has an intimidating name — the sine wave — and a completely ordinary origin.

Picture a dot painted on the rim of a bicycle wheel, and spin the wheel at a steady speed. Now ignore everything except how high the dot is. It rises to the top, falls through the middle, drops to the bottom, comes back. Plot that height against time and you have drawn a sine wave. That is all a sine wave is: going round in circles, viewed from the side.

A · The circle and the wave

Same motion, two views
Radius Laps / sec Start angle Transport
Turn Radius up: the circle grows and so does the wave. That is amplitude, and it is loudness. Turn Laps / sec up: the tone rises. That is frequency, and it is pitch. Turn Start angle: the wave slides sideways without changing shape. That is phase — and on its own you cannot hear it at all, which is why it is the parameter people forget. It matters enormously once two sounds are added together.

Now the formula, which says nothing the picture did not:

the only equation you must memorise x(t) = A · sin(2π f t + φ)

Read it left to right, one piece at a time:

PieceIn the pictureWhat you hear
x(t)The height of the dot at time tThe sample value we store
AThe radius of the circleLoudness
fLaps per secondPitch
tHow long we have been spinning—
2π f tThe angle turned through so far—
φ ("phi")Where the dot startedNothing, alone

So 2π f t is just "one full turn, multiplied by how many turns we have done". Multiply the sine's answer by A to set the size, and you have your number. The module at the top of the page computes exactly this, 48,000 times a second.

How many measurements is enough?

Back to the flipbook. If you photograph a spinning wheel too rarely, something strange happens — you have seen it in films, where wagon wheels appear to spin backwards. The camera catches the wheel a fraction before it completes each turn, so spoke by spoke it seems to creep the wrong way. Nothing is wrong with the wheel or the camera. There simply is not enough information in the photographs to tell the true motion from a slower, backwards one.

Audio has exactly this problem, and there is a precise answer to it. You need more than two measurements per wiggle. Two is the bare minimum to catch a peak and a trough; fewer and the wiggle can masquerade as a slower one. Since we take 48,000 measurements per second, the fastest wiggle we can capture is just under 24,000 per second.

the sampling limit highest recordable frequency = fs / 2

Half the sample rate is called the Nyquist frequency, after Harry Nyquist, and the rule is the Nyquist–Shannon sampling theorem. It is not a quality-of-equipment problem that better hardware will fix. It is arithmetic. Conveniently, human hearing tops out around 20,000 Hz anyway, which is why 44,100 and 48,000 were chosen.

Now the important part. If something faster than Nyquist does sneak in, it does not get quietly dropped and it does not sound like distortion. It comes out as a different, lower note — the wagon wheel, spinning backwards. A 30,000 Hz tone recorded at 48,000 Hz emerges as 48,000 − 30,000 = 18,000 Hz, and nothing downstream can tell it from a tone that was genuinely there. This is called aliasing, and it is the main antagonist of Chapter 3.

Fig 0.2 — Along the bottom, the frequency you asked for. Up the side, the frequency you actually get. They agree until Nyquist, and then the line turns around and comes back down — it "folds". Ask for something too fast and you get something too slow.

B · The fold

Visual only · no audio
Signal Rate
Raise Signal until it passes half of Rate. The grey curve keeps getting faster; the red one — the only wiggle those orange dots can possibly describe — turns around and slows down. The dots are not wrong. They are simply consistent with two different signals, and playback always picks the slower one.

How finely do we measure?

Sample rate is how often we measure. There is a second question: how precisely.

Think of measuring people's heights with a ruler marked only in whole centimetres. Everyone gets rounded to the nearest mark, so every measurement carries a small error. Use a ruler marked in millimetres and the errors shrink. Bit depth is how many marks our ruler has. With b bits there are 2b marks — 16 bits gives 65,536 of them.

Those rounding errors do not disappear; they add a faint hiss underneath the music, called the noise floor. Each extra bit halves the error, which works out to roughly 6 dB of extra range per bit — a unit explained in the next section. 16 bits gives about 96 dB of range, which is why CDs sound clean.

Fig 0.3 — A ruler with only eight marks, exaggerated so you can see it. Notice the result is a staircase, not a curve. The error follows the signal rather than being random, which is why it sounds like grit rather than hiss. Adding a whisper of noise before rounding — called dither — breaks that pattern, trading grit for a slightly louder but far more pleasant hiss.

Because we work in decimals inside the synth rather than whole numbers, this stops being something you think about until the very end. Which is exactly why we do it that way.

Loudness, and why decibels exist

Here is something odd about hearing. Going from an amplitude of 0.1 to 0.2 feels like the same size of jump as going from 0.2 to 0.4 — even though the first added 0.1 and the second added 0.2. Your ears respond to ratios, not differences. Equal multiplications feel like equal steps.

So it is unhelpful to describe loudness by adding and subtracting. We want a scale where "multiply by 2" is always the same distance, no matter where you start. That scale is the decibel, and the maths that produces it is the logarithm:

amplitude to decibels dB = 20 · log₁₀(A)

You will rarely compute this by hand. What you need is the feel of it:

Multiply amplitude by……and you have changed it bySounds like
2+6 dBClearly louder
10 dBNo change
0.5−6 dBClearly quieter
0.1−20 dBMuch quieter, still easily heard
0.001−60 dBNearly silent

Note that the steps add up: −6 dB twice is −12 dB, which is a quarter of the amplitude. That is the whole convenience of the scale.

One consequence to file away for Chapter 7. Two copies of the same signal added together give exactly double the amplitude, so +6 dB. But two different signals of the same level — two different notes, say — only give about +3 dB, because their peaks do not line up. This is why eight voices playing at once are not eight times as loud, and why you must leave headroom when a polyphonic synth adds voices together.

Pitch, and why the keyboard is not evenly spaced

Pitch works on ratios too. Play a note, then play one at exactly double the frequency, and it sounds like "the same note, higher" — that is an octave.

NoteFrequencyRelationship
A155 Hz—
A2110 Hzdouble A1
A3220 Hzdouble A2
A4440 Hzdouble A3 — the standard tuning reference
A5880 Hzdouble A4

Notice the gaps get bigger as you go up: A1 to A2 spans 55 Hz, but A4 to A5 spans 440 Hz. Same musical distance, wildly different distance in hertz. Western music splits each octave into twelve steps called semitones, and to make every step feel equal, each one must multiply by the same amount. Twelve equal multiplications must reach 2, so each semitone multiplies by 21/12 ≈ 1.0595.

Which gives the formula that converts a piano key number into a frequency. Middle A is key 69:

key number to frequency f(n) = 440 · 2^((n − 69) / 12)

In words: start at 440 Hz, and for every key above 69, multiply by the semitone ratio once more.

The one habit to build now

Anything to do with pitch should be expressed as a multiplication, never an addition. Detune is measured in cents — hundredths of a semitone — not in hertz. Filter brightness is adjusted in octaves, not hertz. Vibrato depth is a ratio.

The reason is simple: "add 20 Hz" is a huge wobble on a bass note and inaudible on a high one, whereas "raise by 20 cents" sounds identical everywhere on the keyboard. Get this right at the start and every effect you build later will behave consistently across the whole range.

Everything above, as a program. Use the Rust / Python switch on any listing in this book — it changes every listing on the page, so pick whichever you read more comfortably. Python is the gentler starting point and needs nothing installed for this first one; Rust is what you would ship.

A sine wave, written to a file
// Cargo.toml: hound = "3"
const FS: f32 = 48_000.0;      // measurements per second

// piano key number -> frequency, the formula from above
fn midi_to_freq(n: f32) -> f32 { 440.0 * f32::powf(2.0, (n - 69.0) / 12.0) }

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let spec = hound::WavSpec {
        channels: 1, sample_rate: FS as u32,
        bits_per_sample: 32, sample_format: hound::SampleFormat::Float,
    };
    let mut w = hound::WavWriter::create("tone.wav", spec)?;

    let freq = midi_to_freq(69.0);      // A4, 440 Hz
    let mut phase: f32 = 0.0;        // how far round the circle we are, 0 to 1
    let inc = freq / FS;             // fraction of a lap per measurement

    for _ in 0..(FS as usize * 2) {   // two seconds' worth
        // TAU is one full turn. phase 0..1 becomes an angle 0..one turn.
        let y = (std::f32::consts::TAU * phase).sin() * 0.3;
        w.write_sample(y)?;
        phase += inc;
        if phase >= 1.0 { phase -= 1.0; }   // completed a lap: start the next
    }
    w.finalize()?;
    Ok(())
}
Why keep a running "phase" instead of using the clock?

It looks simpler to write sin(TAU * freq * t) with t counting seconds. Do not. After a few minutes t has grown so large that the computer can no longer tell one measurement from the next, and the tone develops a rasp. Worse, changing freq makes the angle jump, which you hear as a click. Counting laps as we go avoids both. Chapter 1 lets you make that click deliberately, so you know what it sounds like.

Words from this chapter

WordMeaning
SampleOne measurement of the sound. One frame of the flipbook.
Sample rate (fs)How many samples per second. Usually 48,000.
NyquistHalf the sample rate. The fastest wiggle that can be captured.
AliasingA too-fast wiggle coming out as a wrong, slower one. The wagon wheel.
AmplitudeHow big the wiggle is. The radius of the circle. Loudness.
FrequencyHow fast it wiggles, in Hz. Laps per second. Pitch.
PhaseWhere in the cycle you are. The dot's starting angle.
Full scaleAn amplitude of 1.0 — the loudest the system can represent.
Bit depthHow finely each measurement is recorded. The ruler's marks.
Decibel (dB)A loudness scale where multiplying becomes adding.
CentOne hundredth of a semitone. The unit for small tuning changes.
Sine waveThe wiggle you get from going round a circle at a steady speed.
Checkpoint 00

If you are new to this: you can explain why films show wagon wheels spinning backwards, and why that is the same problem as recording a very high note badly.

Going further: you can say why a 25,000 Hz tone recorded at 44,100 Hz comes out at 19,100 Hz, and why "raise the filter by 500 Hz" is a badly posed instruction.

Ch 01 the audio callback · phase accumulators · real-time discipline

The Callback

A synthesizer is not a program that produces sound. It is a function that the sound card calls, on a deadline it does not negotiate.

The contract

The audio device runs on its own high-priority thread. Every so often it hands you a buffer and says: fill this, now. A block of 128 samples at 48 kHz is 2.67 ms of audio, which is also your entire compute budget. Miss it and you do not get a slow frame like in graphics — you get a click, because the driver plays whatever was in the buffer, which is silence or stale data.

Block sizeAt 48 kHzFeels like
641.33 msPlayable on a controller; unforgiving of any hitch
1282.67 msThe usual default. Good target.
51210.7 msNoticeable lag when playing; fine for playback
204842.7 msUnplayable, but very hard to underrun

Note that latency is at least two blocks, not one: you are filling block n + 1 while block n plays. Add the driver's own buffering and the round trip is typically three to five blocks.

The phase accumulator

The core primitive of every oscillator on this page is four lines long. Keep a phase in [0, 1). Add an increment each sample. Wrap. Map the phase to a waveform.

phase increment Δφ = f / fs φ ← (φ + Δφ) mod 1

Normalising phase to [0,1) rather than [0, 2π) is not cosmetic. The wrap becomes a subtraction of exactly 1.0, which is exact in floating point; wrapping at 2π accumulates error. It also makes Δφ directly meaningful: it is the fraction of a cycle traversed per sample, and it is the parameter PolyBLEP needs in Chapter 3.

C · Phase accumulator

The ramp, and what it becomes
Freq Level Shape Reset phase on freq change Transport
Set Reset phase to Yes, start playback, then sweep the frequency knob. Every step now restarts the cycle at zero, which is a discontinuity, which is a click. Set it back to No and sweep again: same frequencies, no clicks, because phase is continuous across the change. This is the whole argument for accumulating rather than recomputing.

The rules inside the callback

The audio thread must never block. Not for a lock, not for a page fault, not for the allocator, not for a log line. If it blocks, the deadline passes and you hear it.

  • No heap allocation. Preallocate every buffer, voice, and delay line before the stream starts. In Rust this means no Vec::push, no Box::new, no String, no collecting iterators.
  • No locks. A Mutex held by the UI thread during a GC pause or a scheduler preemption will stall the audio thread. Use lock-free single-producer queues (ringbuf) or atomics for scalar parameters.
  • No I/O, no syscalls, no println!.
  • No unbounded work. Worst case matters, not average. Eight voices must all fit in the budget even when every one of them is sounding.
  • Watch for denormals. Filter and reverb tails decay toward zero and enter subnormal float territory, where some CPUs take a large penalty per operation. Enable flush-to-zero, or add a tiny DC offset to the signal.

The demos on this page obey these rules. Each one runs in an AudioWorkletProcessor, which is the browser's audio thread; parameters arrive as messages and are copied into plain fields, and the analysis frames sent back to the page are written into a buffer allocated once at construction.

A real-time oscillator
// cpal = "0.15", ringbuf = "0.3"
struct Osc { phase: f32, inc: f32 }

impl Osc {
    fn set_freq(&mut self, f: f32, fs: f32) { self.inc = f / fs; }
    fn next(&mut self) -> f32 {
        let p = self.phase;
        self.phase += self.inc;
        if self.phase >= 1.0 { self.phase -= 1.0; }
        (std::f32::consts::TAU * p).sin()
    }
}

let device = cpal::default_host().default_output_device().unwrap();
let config = device.default_output_config()?.config();
let fs = config.sample_rate.0 as f32;
let ch = config.channels as usize;

let mut osc = Osc { phase: 0.0, inc: 0.0 };
osc.set_freq(220.0, fs);

let stream = device.build_output_stream(
    &config,
    move |out: &mut [f32], _| {
        // This closure IS the audio thread. Allocate nothing here.
        for frame in out.chunks_mut(ch) {
            let y = osc.next() * 0.25;
            for s in frame.iter_mut() { *s = y; }
        }
    },
    |err| eprintln!("stream error: {err}"),
    None,
)?;
stream.play()?;
A caveat if you are following in Python

The Python listing works, and it is a fine way to learn — but Python cannot meet a hard real-time deadline the way Rust can. The garbage collector can pause you at any moment, the global interpreter lock can hold you up, and allocation happens constantly whether you asked for it or not. You will hear occasional clicks under load, and no amount of tuning removes them entirely.

The practical approach is to write your DSP with NumPy over whole blocks rather than sample by sample, keep the callback short, and treat the odd dropout as the price of a quick experiment. When you want an instrument rather than an experiment, port it — which is much easier than starting in Rust, because by then you will know exactly what you are building.

Parameter changes are steps

Sending a new gain from the UI and assigning it directly means the signal jumps between two blocks. That jump is a click, exactly like the phase reset above. Chapter 7 fixes it properly with a one-pole smoother; for now, know that any parameter multiplying the signal needs slewing, and any parameter that only sets a frequency usually does not.

Checkpoint 01

You have a tone playing from a real audio callback that you can retune live without clicks, and you can state your compute budget in milliseconds.

Ch 02 harmonic series · fourier · the DFT · windowing

Fourier

Timbre is spectrum. Once you can see a sound as a set of partials, most of synthesis becomes a question of which partials to make and which to remove.

The claim

Any periodic signal, however jagged, is a sum of sinusoids at integer multiples of its fundamental frequency. Those multiples are its harmonics or partials. The physical reason is that a bounded vibrating system — a string, an air column — only supports standing waves whose ends fit the boundary, and those fit at integer divisions of the length.

The three canonical synthesizer waveforms have famously simple recipes:

WaveHarmonics presentAmplitude of harmonic nCharacter
Sawtoothall1/nBrightest. Brass, strings.
Squareodd only1/nHollow, woody. Clarinet-ish.
Triangleodd only1/n²Nearly a sine. Soft flutes.
Sinethe first, alone—No timbre to speak of.

The 1/n² rolloff of the triangle is 12 dB per octave of harmonic number against the sawtooth's 6, which is why a triangle sounds so much darker even though both contain infinitely many partials.

Fig 2.1 — The first five terms of a sawtooth. Each fits a whole number of cycles into the same period, which is precisely why their sum is periodic at the fundamental.
Fig 2.2 — Partial sums with 1, 3, 7 and 63 harmonics. Note the overshoot that refuses to shrink at the discontinuity — Gibbs phenomenon. It is not an error; it is what band-limiting a step actually looks like, and Chapter 3's PolyBLEP is a cheap way of drawing that shape deliberately.

D · Additive workbench

16 sine partials · build the waveform by hand
f0 Level Phase Load recipe Transport
Load Saw 1/n and listen; then pull out every even slider and hear it become a square. Now turn Phase up. The waveform on the left changes completely while the spectrum on the right does not move — and the sound barely changes. For steady tones the ear is largely deaf to the relative phase of partials. It is not deaf to it during attacks, or when two sources are mixed.

From series to transform

The Fourier series describes periodic signals. Real sounds are not periodic forever, so we chop them into short frames and analyse each one with the Discrete Fourier Transform. The DFT correlates the frame against a complex exponential at every candidate frequency:

discrete fourier transform X[k] = Σn=0..N−1 x[n] · e−j2πkn/N

Euler's formula, e^(jθ) = cos θ + j sin θ, is why a single complex multiply captures both the in-phase and quadrature components at once — which is to say, both how much of that frequency is present and at what phase. The magnitude |X[k]| is what we plot; arg(X[k]) is the phase we usually discard.

The FFT is the same transform computed in O(N log N) instead of O(N²) by exploiting the symmetry of those exponentials. Three practical consequences:

  • Bin spacing is fs/N. At 48 kHz with N = 2048, each bin is 23.4 Hz wide. You cannot resolve two partials closer than that; you need a longer frame, which means worse time resolution. That trade is not a limitation of the algorithm, it is a property of the universe.
  • Only the first N/2 bins are meaningful. The rest are the conjugate mirror — the same Nyquist limit, seen from the other side.
  • Window your frame. Chopping a signal abruptly is multiplying by a rectangle, which convolves the spectrum with a very leaky kernel; a single pure sine smears across dozens of bins. A Hann window fixes most of it. Every spectrum on this page is Hann-windowed.

Additive synthesis, and why nobody uses it alone

You now have a complete synthesis method. Want a sawtooth? Sum sines at 1/n up to Nyquist. It is exact, it never aliases, and it is the ground truth against which Chapter 3's cheaper methods are judged.

It is also brutally expensive. A 55 Hz sawtooth at 48 kHz needs 436 partials — 436 sine evaluations per sample, per voice. Eight voices is 3,500 sines every 20 microseconds. That is why the rest of this book is, in a sense, a catalogue of ways to fake it.

Analysing your own output
// rustfft = "6"
use rustfft::{FftPlanner, num_complex::Complex};

fn spectrum_db(x: &[f32], fs: f32) -> Vec<(f32, f32)> {
    let n = x.len();
    let mut buf: Vec<Complex<f32>> = x.iter().enumerate()
        .map(|(i, &s)| {
            // Hann window — without this a pure sine leaks everywhere
            let w = 0.5 - 0.5 * (std::f32::consts::TAU * i as f32 / (n as f32 - 1.0)).cos();
            Complex::new(s * w, 0.0)
        }).collect();

    FftPlanner::new().plan_fft_forward(n).process(&mut buf);

    buf.iter().take(n / 2).enumerate().map(|(k, c)| {
        let mag = c.norm() / (n as f32 / 4.0);
        (k as f32 * fs / n as f32, 20.0 * mag.max(1e-9).log10())
    }).collect()
}
Do this before moving on

Render a sawtooth with your Chapter 0 code, run the FFT over it, and print the ten loudest bins. If they are not at f0, 2f0, 3f0… with roughly 6 dB drops between successive octaves of harmonic number, something is wrong — and finding out what is worth more than reading the next chapter.

Checkpoint 02

You can look at a magnitude spectrum and predict what it will sound like, and you can explain why a longer analysis window gives sharper frequency resolution and blurrier timing.

Ch 03 aliasing · polyblep · wavetables · interpolation

Band-Limiting

The most obvious way to make a sawtooth is also the wrong one. This is the chapter where naive code starts to sound like broken code, and where most self-taught synth builders quietly give up.

Why the obvious thing fails

A sawtooth is a ramp from −1 to 1 that resets instantly. You already have a phase in [0,1), so y = 2·phase − 1 and you are done. Play it at 55 Hz and it sounds fine. Play it at 2 kHz and it sounds like a mistake.

The reason is the reset. An instantaneous jump has energy at every frequency — that is what Fig 2.2 was showing. An ideal sawtooth contains harmonics forever, and at 2 kHz only eleven of them fit below Nyquist. The rest do not vanish. They fold, per Fig 0.2, landing at frequencies that bear no harmonic relationship to the note you played.

That last part is what makes it so ugly. Distortion that adds harmonics sounds like distortion. Aliasing adds inharmonic partials, and worse, they move the wrong way: play a chromatic run upward and the aliased components descend, producing a shimmering metallic counter-melody that follows you around. Once you have heard it you will never mistake it for anything else.

E · Four ways to make a sawtooth

Same pitch · same waveform · very different spectra

Dotted verticals mark the true harmonics of f0. Anything between them is aliasing.

Construction f0 Level Sweep 55 Hz → 7 kHz Transport
The test that matters: turn Sweep on with Naive ramp selected and listen for thirty seconds. You will hear partials descending while the pitch rises. Switch to PolyBLEP mid-sweep without stopping. Same waveform on the left, and the noise is gone. Additive is the ground truth — perfectly clean, and roughly a hundred times the cost.

PolyBLEP

We cannot remove aliasing after it happens; the folded partials are indistinguishable from real ones. We have to avoid generating them. The observation behind BLEP — band-limited step — is that a naive waveform differs from its band-limited ideal only in a tiny neighbourhood of each discontinuity. So compute the naive waveform, then add a correction shaped like the difference.

A properly band-limited step is the integral of a sinc, which is expensive and infinite in extent. PolyBLEP replaces it with a two-piece quadratic covering just one sample either side of the jump. It is not exact — it typically knocks 40–60 dB off the aliasing rather than eliminating it — but it costs a handful of arithmetic operations, which is why nearly every virtual-analogue oscillator shipping today uses it.

PolyBLEP — the whole algorithm
/// t  = phase distance from the discontinuity, in [0,1)
/// dt = phase increment per sample = freq / sample_rate
fn poly_blep(mut t: f32, dt: f32) -> f32 {
    if t < dt {
        t /= dt;
        t + t - t * t - 1.0            // just after the jump
    } else if t > 1.0 - dt {
        t = (t - 1.0) / dt;
        t * t + t + t + 1.0            // just before the jump
    } else {
        0.0                            // nowhere near a jump: leave it alone
    }
}

fn saw(&mut self) -> f32 {
    let (p, dt) = (self.phase, self.inc);
    self.advance();
    (2.0 * p - 1.0) - poly_blep(p, dt)
}

fn square(&mut self, pw: f32) -> f32 {
    let (p, dt) = (self.phase, self.inc);
    self.advance();
    let mut y = if p < pw { 1.0 } else { -1.0 };
    y += poly_blep(p, dt);                       // rising edge at phase 0
    y -= poly_blep((p - pw + 1.0) % 1.0, dt);    // falling edge at phase pw
    y
}

A square has two discontinuities per cycle, so it needs two corrections with opposite signs. A triangle has no discontinuity in value at all — only in slope — so it needs a different correction (PolyBLAMP), or, more cheaply, you integrate a band-limited square with a leaky one-pole. That is what this book's triangle does.

Wavetables

The other mainstream answer trades memory for arithmetic. Precompute one cycle of the waveform into a table by additive synthesis — expensive, but done once at startup — and then just read the table back at the right rate.

The catch is that a table built with 400 harmonics is only safe for low notes. So you build one table per octave, each containing only the harmonics that still fit under Nyquist at the top of that octave, and select the appropriate table by pitch. Eleven tables of 2048 floats is 88 KB — nothing — and playback is one lookup and one interpolation regardless of how rich the waveform is.

MethodCost per sampleAliasingFlexible shape?
Naive1 multiplyCatastrophicYes
PolyBLEP~10 ops−40 to −60 dBYes — PWM and sync work naturally
Wavetable1 lookup + lerpExcellentOnly precomputed shapes
AdditiveN sinesNoneTotal, per-partial
Interpolation is not free either

Linear interpolation between table entries is itself a lowpass filter with a small amount of distortion — the error shows up as a noise floor around −60 to −70 dB with a 2048-point table. Cubic (Catmull-Rom) interpolation buys you roughly another 20 dB for about three times the arithmetic. The same choice reappears in Chapter 8, where a chorus reads a delay line at a fractional position.

Two things PolyBLEP gives you for free

Pulse-width modulation. Move the falling edge of a square away from the halfway point and the spectrum changes dramatically — at 50% the even harmonics cancel completely, and as the width narrows they return. Sweep it slowly with an LFO and you get the thick, breathing texture that defines a great deal of 1980s synth pop. Because PolyBLEP corrects each edge independently, this costs nothing extra.

Hard sync. Reset oscillator B's phase every time oscillator A completes a cycle. B's pitch is then locked to A, but its timbre is set by B's own frequency, which you can sweep freely. The forced reset is a discontinuity, so it needs its own BLEP correction — and it produces the tearing, aggressive sound that made the sync lead a genre unto itself.

Checkpoint 03

Your oscillator plays a four-octave chromatic run and the top octave sounds like the bottom octave, only higher. You can point at a spectrum and say which partials are real.

Ch 04 ADSR · exponential curves · gates · velocity · MIDI

Envelopes

A continuous tone is not a note. What turns one into the other is a shape in time — and the ear cares more about the first 50 milliseconds of that shape than about almost anything else in the signal.

Why attack transients carry identity

Record a piano note and a trumpet note at the same pitch and loudness, then splice off the first tenth of a second of each. Listeners struggle to tell them apart. The steady-state spectra of many instruments are surprisingly similar; the attack — how fast, in what order the partials arrive, with what noise — is where the instrument lives. This is why a synth patch with a lazy envelope sounds generic no matter how good the oscillator.

ADSR

The standard shape has four parameters and, importantly, is driven by a gate: a boolean that is true while the key is held.

  • Attack — time from 0 to full, starting at note-on.
  • Decay — time from full down to the sustain level.
  • Sustain — a level, not a time. Held for as long as the gate is true.
  • Release — time from wherever the envelope is when the gate falls, down to zero.

The asymmetry catches everyone once: three times and one level. It follows from the gate being the thing in charge. Sustain has no duration because its duration is the player's business.

Fig 4.1 — The same four parameters, drawn with exponential segments and with linear ones. Both reach the same places at the same times. Only one sounds like a real instrument decaying.

Why the curve has to be exponential

Physical systems lose energy in proportion to how much energy they have, which produces exponential decay. Our perception of loudness is also roughly logarithmic. A linear ramp down to zero therefore sounds like it hangs at a constant loudness and then suddenly disappears at the end; an exponential decay sounds like a decay.

The cheap and idiomatic way to get one is a one-pole filter aimed at a target:

one-pole envelope segment v ← target + (v − target) · c c = exp(−1 / (τ · fs))

For decay and release, target is the destination. For attack there is a trick: aim at something above 1.0 — say 1.25 — and stop when you cross 1.0. Aim exactly at 1.0 and you never arrive, because an exponential approaches asymptotically. The overshoot target also gives the attack a pleasantly convex shape rather than a decelerating one.

F · Envelope generator

Hold the gate · watch the shape · hear the difference
Attack Decay Sustain Release Pitch Segment curve On note-off Play Transport
Press and hold the gate button. The shaded region on the left shows the gate; the envelope only reaches release when you let go. Then set On note-off to Hard cut and try again — that snap is what happens if you stop a voice by setting its gain to zero instead of releasing it. Every audible click in a synthesizer is a discontinuity somewhere, and this is the most common one.

Velocity, and what to do with it

MIDI note-on carries a velocity from 1 to 127. Mapping it straight to amplitude is the obvious move and the least convincing one, because on a real instrument playing harder does not merely make the sound louder — it makes it brighter, because more energy goes into the higher modes. So route velocity to amplitude and to filter cutoff, which is exactly what the synth in Chapter 7 does.

Two details worth getting right. First, map velocity through a curve rather than linearly; (v/127)² or similar feels far more natural on most controllers. Second, a note-on with velocity 0 is defined by the MIDI spec to mean note-off, and a surprising amount of hardware relies on that. Handle it or you will hang notes.

MIDI in, without touching the audio thread
// midir = "0.9", ringbuf = "0.3"
enum Ev { On { note: u8, vel: u8 }, Off { note: u8 }, Cc { num: u8, val: u8 } }

let rb = ringbuf::HeapRb::<Ev>::new(256);
let (mut tx, mut rx) = rb.split();

// --- MIDI thread: parse and push. Never touches the synth. ---
let _conn = midi_in.connect(&port, "in", move |_stamp, msg, _| {
    if msg.len() < 3 { return; }
    let ev = match msg[0] & 0xF0 {
        0x90 if msg[2] > 0 => Ev::On { note: msg[1], vel: msg[2] },
        0x90 | 0x80        => Ev::Off { note: msg[1] },   // vel 0 == note off
        0xB0               => Ev::Cc { num: msg[1], val: msg[2] },
        _ => return,
    };
    let _ = tx.push(ev);          // full queue drops, never blocks
}, ())?;

// --- audio thread: drain, then render. No locks, no allocation. ---
move |out: &mut [f32], _| {
    while let Some(ev) = rx.pop() {
        match ev {
            Ev::On { note, vel } => synth.note_on(note, vel as f32 / 127.0),
            Ev::Off { note }     => synth.note_off(note),
            Ev::Cc { num, val }  => synth.control(num, val as f32 / 127.0),
        }
    }
    synth.render(out);
}
Sample-accurate timing, when you need it

Draining the whole queue at the top of the block quantises every event to the block boundary — up to 2.67 ms of jitter, which is audible on fast passages. The fix is to timestamp events with a sample offset within the block and split rendering at those offsets. Worth doing eventually. Not worth doing first.

Checkpoint 04

You press a key on a controller and hear a shaped note; you release it and hear it decay through its release stage. No clicks at either end.

Ch 05 difference equations · poles & zeros · zero-delay feedback · ladders

Filters

The longest chapter, and the one that repays study. Oscillators give a synthesizer its raw material; the filter is what makes it expressive.

Subtractive synthesis in one sentence

Start with a harmonically rich waveform, remove parts of its spectrum, and modulate what you remove over time. That is the entire architecture of the Minimoog, the Prophet-5, the TB-303, and most software synths since.

The difference equation

A digital filter computes each output sample from recent inputs and recent outputs:

the general form y[n] = b₀x[n] + b₁x[n−1] + b₂x[n−2] − a₁y[n−1] − a₂y[n−2]

The b terms are feedforward and create zeros: frequencies that get cancelled. The a terms are feedback and create poles: frequencies that get emphasised. A filter with only b terms is FIR — finite impulse response, unconditionally stable, and it takes many taps to be selective. Add feedback and it becomes IIR, which is dramatically more efficient and can also explode.

Fig 5.1 — Feed in a single 1.0 followed by zeros and record what comes out: the impulse response. It characterises the filter completely. Its Fourier transform is the frequency response — which is how every response curve in this chapter is measured, rather than derived from a formula.

Poles, zeros, and the unit circle

Apply the z-transform (substituting z⁻¹ for "delayed by one sample") to the difference equation and you get a ratio of polynomials:

transfer function H(z) = (b₀ + b₁z⁻¹ + b₂z⁻²) / (1 + a₁z⁻¹ + a₂z⁻²)

Roots of the numerator are zeros; roots of the denominator are poles. Evaluate H around the unit circle z = e^(jω) and you have the frequency response — the magnitude at each frequency is the product of distances to the zeros divided by the product of distances to the poles. That geometric picture is worth internalising: a pole near the circle means a nearby frequency gets divided by something tiny, hence a resonant peak.

Fig 5.2 — A lowpass biquad. Two zeros sit at Nyquist (killing the top), a conjugate pole pair sits near the circle at the cutoff angle. Push the poles onto the circle and the filter oscillates forever; push them outside and the output diverges. Poles inside the unit circle is the entire stability condition.

The one-pole

Start here, because you can derive it. An RC lowpass is a first-order differential equation; the simplest discretisation gives:

one-pole lowpass y[n] = y[n−1] + g · (x[n] − y[n−1]) g = 1 − e^(−2π fc / fs)

One multiply, two adds, one state variable. It rolls off at 6 dB/octave, has no resonance, and cannot go unstable for g in [0,1]. Subtract its output from its input and you have a highpass for free. You will use this constantly — for DC blocking, for damping in a reverb, for smoothing parameters, for the envelope in Chapter 4.

The biquad, and its limitation

Two poles and two zeros gets you 12 dB/octave and genuine resonance. The RBJ cookbook gives closed forms for lowpass, highpass, bandpass, notch, shelves and peaks in terms of cutoff and Q, and they are exact — a biquad matched to an analogue prototype is indistinguishable from it, at a fixed cutoff.

The trouble is modulation. Those coefficients are a nonlinear function of cutoff, and the filter's two state variables carry a memory that was consistent with the old coefficients. Recompute them every sample while an envelope sweeps and the state no longer matches; you get zipper noise at best and loud transients at worst. Recompute them once per block and you get stepping. Biquads are superb for EQ and poor for the thing a synth filter is for.

Zero-delay feedback, and why it exists

An analogue state-variable filter is a loop of two integrators with feedback. Translating it naively means the feedback needs the output before the output has been computed — a delay-free loop, which code cannot express. The old solution was to insert a one-sample delay in the loop. It works, but that delay is a phase error that grows with frequency, so as you raise cutoff the resonant peak drifts flat and the filter eventually blows up.

The modern solution, developed properly in Vadim Zavalishin's The Art of VA Filter Design, is to solve the loop algebraically. Write the integrator in its trapezoidal (TPT) form, substitute, and the instantaneous feedback resolves into a set of coefficients you can precompute. The result is a filter that stays accurate and stable while cutoff is modulated at audio rate.

TPT SVF — one sample g = tan(π·fc/fs) k = 1/Q a₁ = 1/(1 + g(g+k)) a₂ = g·a₁ a₃ = g·a₂
The filter you should actually ship
pub struct Svf { g: f32, k: f32, a1: f32, a2: f32, a3: f32, ic1: f32, ic2: f32, fs: f32 }

impl Svf {
    pub fn set_cutoff(&mut self, fc: f32) {
        self.g = (std::f32::consts::PI * fc.clamp(12.0, self.fs * 0.47) / self.fs).tan();
        self.update();
    }
    pub fn set_res(&mut self, r: f32) {
        self.k = 2.0 - 2.0 * r.clamp(0.0, 0.985);   // k -> 0 is self-oscillation
        self.update();
    }
    fn update(&mut self) {
        self.a1 = 1.0 / (1.0 + self.g * (self.g + self.k));
        self.a2 = self.g * self.a1;
        self.a3 = self.g * self.a2;
    }
    /// Returns (lowpass, bandpass, highpass) — all three, same cost.
    pub fn process(&mut self, x: f32) -> (f32, f32, f32) {
        let v3 = x - self.ic2;
        let v1 = self.a1 * self.ic1 + self.a2 * v3;
        let v2 = self.ic2 + self.a2 * self.ic1 + self.a3 * v3;
        self.ic1 = 2.0 * v1 - self.ic1;
        self.ic2 = 2.0 * v2 - self.ic2;
        (v2, v1, x - self.k * v1 - v2)
    }
}

Notice that lowpass, bandpass and highpass all fall out of the same state — and that notch = high + low and peak = high − low. One filter, five responses, no extra arithmetic. That is why it is called a state-variable filter.

Fig 5.3 — All four outputs of the same SVF at 1 kHz with moderate resonance, measured from its impulse response. The resonant bump appears in every output because it is a property of the poles, which are shared.

G · Filter bench

Four topologies, measured live from their impulse responses

Solid curve = the filter you are hearing. Dashed = the other three at the same settings.

Topology Response Source Cutoff Res Env amt LFO amt LFO rate Pitch Envelope Transport
Three experiments worth doing properly. One: set resonance near 0.95 and sweep cutoff slowly — the classic sound. Two: switch between Biquad and TPT SVF while sweeping fast; the biquad stutters and thumps where the SVF glides. Three: set Env amt to 3 octaves with a short decay and hit Trigger repeatedly — that percussive downward sweep is most of what people mean by "a synth sound". Then compare Ladder against TPT SVF at high resonance: the ladder's saturator makes the peak fatten and compress instead of simply screaming.

The ladder, and nonlinearity

Moog's transistor ladder is four one-pole stages in series with global negative feedback. Four poles means 24 dB/octave, and the feedback path sets resonance. In a purely linear implementation, pushing feedback past the critical point makes it self-oscillate into an ever-growing sine.

In the real circuit, the transistors saturate. That single nonlinearity does several things at once: it limits self-oscillation to a stable amplitude, it generates harmonics that thicken the resonant peak, and it makes the filter respond differently to loud and quiet inputs. Put a tanh in the feedback path of your ladder and you get most of it. Leave it out and you have a filter that is technically correct and sounds sterile.

Nonlinearity means aliasing again

Saturation generates harmonics, and harmonics above Nyquist fold — the same problem as Chapter 3, arriving from a new direction. The standard mitigation is oversampling: upsample 2× or 4×, run the nonlinear stage, filter, downsample. It is the main reason a good software filter costs so much more than the arithmetic in its difference equation suggests.

Keyboard tracking

A fixed cutoff removes a different proportion of the harmonics from a low note than from a high one, so high notes sound progressively duller. The fix is to scale cutoff with pitch:

key tracking, amount in [0,1] fc_effective = fc · (f_note / f_ref) ^ amount

At amount 1.0 the filter tracks the keyboard exactly and timbre is constant across the range; at 0 it is fixed. Somewhere between 0.3 and 0.7 usually sounds most like an instrument. This is your first real taste of the principle that governs Chapter 6 — modulation belongs in the exponent.

Checkpoint 05

You have a saw → SVF → amp voice with an envelope on cutoff, it sweeps musically without stepping or blowing up, and you can explain what a pole is and why yours are inside the circle.

Ch 06 LFOs · phase modulation · sidebands · routing

Modulation

Nothing in an interesting sound holds still. Modulation is the machinery for making things move — and when you speed it up past about 20 Hz it stops being movement and becomes timbre.

Low-frequency oscillators

An LFO is the same phase accumulator from Chapter 1 running below the hearing threshold. Its output is a control signal, and where you send it decides what it is called:

DestinationNameTypical depth
Oscillator pitchVibrato±10 to 50 cents, 4–7 Hz
AmplitudeTremolo±10–30%, 3–8 Hz
Filter cutoffWobble / auto-wah±0.5 to 3 octaves
Pulse widthPWM±30%, 0.2–2 Hz
Delay timeChorus / flanger±2–5 ms, 0.3–1 Hz

Two implementation notes that matter more than they look. Depth is exponential for anything pitch-like — a vibrato of "±20 Hz" is enormous on a bass note and inaudible on a high one, whereas "±20 cents" is the same everywhere. And free-running versus retriggered is a musical decision: a free-running LFO makes repeated notes differ from one another, which sounds alive; a retriggered one makes them identical, which sounds tight. Offer both.

Sample-and-hold deserves a mention: an LFO that latches a random value once per cycle. Route it to cutoff and you have the bubbling sound that defines a certain kind of sequenced electronic music.

FM is really PM

Push a modulator into the audio range and something different happens. Rather than hearing the pitch move, you hear a new, static timbre — because the modulation is now fast enough to create sidebands.

Here is the thing everyone gets wrong: what Yamaha sold as FM, and what almost every "FM" synth implements, is phase modulation. The modulator is added to the carrier's phase, not to its frequency:

phase modulation y(t) = sin(2π fc t + I · sin(2π fm t))

The two are mathematically related — frequency is the derivative of phase — but PM is far better behaved in practice. True FM integrates the modulator, so any DC offset in it makes the carrier's pitch drift, and stacking operators compounds the problem. PM has no such issue, which is why it won.

Sidebands

The spectrum of the equation above is a carrier plus pairs of sidebands at fc ± k·fm for every integer k. The amplitude of the k-th pair is the Bessel function of the first kind, Jk(I), where I is the modulation index.

You do not need to compute Bessel functions, and you should not try to memorise them. You need three facts from them:

  • Index controls brightness. At I = 0 only the carrier exists — a pure sine. Raising I pushes energy outward into higher-order sidebands. Roughly, significant sidebands extend to about k ≈ I + 1.
  • The carrier is not always the loudest component. J₀ passes through zero at I ≈ 2.4, so at that index the fundamental disappears entirely. This is a real effect you can hear, and it is why FM patches can sound like they change pitch when they do not.
  • Negative-frequency sidebands fold and phase-invert. When fc − k·fm goes below zero it reflects around DC, and if it lands on an existing partial it can cancel it. Not a bug — this is a large part of what makes FM spectra so distinctive and so hard to predict by ear.
Fig 6.1 — Carrier 400 Hz, ratio 1:1, at four modulation indices. The bandwidth grows with index, and the level of any given partial does not move monotonically — that is the Bessel behaviour.

The ratio decides harmonic or not

Sidebands sit at fc ± k·fm. If fm/fc is a simple integer ratio, every sideband lands exactly on a harmonic of the carrier, and the result is a pitched tone. If the ratio is irrational, the sidebands fall between the harmonics, the result is inharmonic, and your ear reports "bell", "gong", or "metal".

RatioResult
1 : 1Sawtooth-like, all harmonics. Brass and reeds.
2 : 1Odd harmonics only. Clarinet, hollow.
3 : 1, 5 : 1Sparse harmonic spectra. Woodwind-ish.
1.41 : 1Inharmonic. Bells, tubular metal.
3.14 : 1Deeply inharmonic. Gongs, sound design.

Add an envelope on the index and you have the FM electric piano: a bright inharmonic strike that settles into a nearly pure sine as the index falls. Two operators and two envelopes reproduce one of the most recognisable sounds of the 1980s.

H · Two-operator phase modulation

One modulator, one carrier, and a great deal of spectrum
Carrier Ratio Index Feedback Index dec Index sus Amp dec Index envelope Note Transport
Set Ratio to exactly 2.00 and sweep Index from 0 upward — watch sidebands appear one pair at a time, landing precisely on the dotted harmonic lines. Now nudge the ratio to 2.13. The lines and the peaks separate, and the sound turns to metal. For the classic electric piano: ratio 1.00, index around 5, Index dec near 0.25 s, Index sus near 0.05, and press Strike.

Ring modulation, and routing

Multiplying two signals rather than adding gives ring modulation: sum and difference frequencies only, no carrier. With two sines you get exactly two partials at f₁ ± f₂ — the fastest route to inharmonic, clangorous, distinctly artificial tones. Amplitude modulation is the same operation with a DC offset on one input, which keeps the carrier in the output.

Once you have several sources and several destinations you need a modulation matrix: a list of (source, destination, depth) triples evaluated per sample or per block. Build this abstraction before you have five modulations, not after. Hard-coding "LFO1 goes to cutoff" into the voice is the single most common architectural mistake in a first synth, and unpicking it later touches every line of the render loop.

Checkpoint 06

You can build a bell and an electric piano from two operators, and you can explain why ratio 2.13 sounds metallic when ratio 2.00 does not.

Ch 07 voices · stealing · smoothing · denormals · budgets

Voices

Everything so far made one sound at a time. Turning that into an instrument is mostly an exercise in resource management — which is the part of this book your systems instincts already cover.

The voice

A voice is one complete signal chain: oscillators, filter, envelopes, and the state they need. Polyphony is N voices summed. That is the whole idea, and it is worth stating plainly because the interesting problems are all about allocation rather than DSP.

Design decision to make early: every voice owns all its state. No shared filter, no shared envelope. It seems wasteful and it is not — the alternative is a tangle where one note's release affects another's tone. The only things reasonably shared are the global LFOs and the effects, which sit after the summing point.

Allocation and stealing

On note-on you need a voice. In order of preference:

  1. A voice already playing this exact note — retrigger it, so repeated notes do not stack up into a volume spike.
  2. Any completely idle voice.
  3. Failing both, steal one.

Stealing policy is audible. Taking the oldest voice is the standard choice and works well for keyboard playing. Taking the quietest — the one furthest into its release — is less noticeable but costs you a scan. Taking the lowest note is wrong for bass lines and right for pads. Whatever you pick, do not simply cut the stolen voice off: give it a very short forced release, a few milliseconds, before restarting. Otherwise you get a click on every stolen note, which is exactly when the texture is busiest and least able to hide it.

Fig 7.1 — Four voices, four held notes, and a fifth note arriving. Voice 1 is the oldest, so it is taken; its C4 ends early. Note also that a voice stays busy after note-off, for the whole release stage — which is why a four-voice synth cannot reliably play four-note chords with a long release.

Smoothing

Chapter 1 warned about this; here is where it bites. Any parameter that multiplies the signal — gain, mix, effect depth — must be slewed, or every knob movement is a staircase of small steps, each one a discontinuity. The audible result is called zipper noise and it sounds exactly like the name.

one-pole parameter smoother v ← target + (v − target) · c c = exp(−1 / (τ · fs))

A time constant around 5–20 ms is right for gains. Parameters that only set a frequency — oscillator pitch, filter cutoff — often do not need it, because the filter or oscillator absorbs the step; but cutoff on a high-resonance filter is an exception, and glide (portamento) is just a deliberately slow smoother on pitch.

Denormals

A filter's tail decays exponentially toward zero and eventually reaches values around 10−38, where floats become subnormal. On some x86 CPUs every operation on a subnormal takes a microcoded slow path, and a reverb decaying into silence can cost more than the entire rest of your synth. Symptoms: CPU usage that spikes after you stop playing.

Two fixes. Set the CPU's flush-to-zero and denormals-are-zero flags once at stream startup, or add a tiny constant — around 10−20 — to the signal at a few points in the feedback paths. The first is cleaner; the second is portable.

Block processing

Per-sample code like everything in this book is the clearest way to learn. Once it works, the standard optimisation is to hoist everything that does not change per sample out of the inner loop: coefficient updates, envelope stage checks, parameter reads. Many synths update modulation once every 8, 16 or 32 samples — a "control rate" — and interpolate between updates. The saving is large, the audible cost is usually nil, and the risk is that a fast envelope gets quantised into steps. Keep amplitude envelopes at full rate.

I · The synthesizer

8 voices · 2 oscillators + sub + noise · 2 envelopes · LFO
Preset Osc 1 Osc 2 Osc mix Detune Interval Sub Noise Cutoff Res Env → fc Keytrack Filter Mode
Amp A Amp D Amp S Amp R Filt A Filt D Filt S Filt R LFO rate → Pitch → Cutoff Glide Voices Master Panic Transport
Click this panel first, then play with Z–M and Q–P, or click the keys directly. The meters above the keyboard show voice state: amber for held, teal for releasing. Turn Voices down to 2 and play a four-note chord — you can watch and hear the stealing happen. Turn Amp R up to several seconds and the same thing happens at four voices, because releasing notes still occupy them.
The voice allocator
pub struct Synth { voices: [Voice; 8], counter: u64 }

impl Synth {
    pub fn note_on(&mut self, note: u8, vel: f32) {
        self.counter += 1;
        let stamp = self.counter;

        // 1. same note already sounding -> retrigger it
        if let Some(v) = self.voices.iter_mut()
            .find(|v| v.note == note && v.held) {
            return v.trigger(note, vel, stamp);
        }
        // 2. a free voice
        if let Some(v) = self.voices.iter_mut().find(|v| !v.active()) {
            return v.trigger(note, vel, stamp);
        }
        // 3. steal the oldest. min_by_key, not sort - no allocation.
        let v = self.voices.iter_mut()
            .min_by_key(|v| v.stamp).unwrap();
        v.steal(note, vel, stamp);   // 3 ms forced fade, then retrigger
    }

    pub fn render(&mut self, out: &mut [f32]) {
        out.fill(0.0);
        for v in self.voices.iter_mut().filter(|v| v.active()) {
            v.add_into(out);     // accumulate, never allocate a temp buffer
        }
    }
}
Measure the worst case

Time your callback with all voices sounding, maximum resonance, and every effect engaged — not the average. Divide by the block duration to get a load fraction. Anything above about 0.5 will glitch on a busy machine, because you are sharing the CPU with a browser, a DAW, and a scheduler that does not know your deadline exists.

Checkpoint 07

You can play chords. Holding the sustain pedal through a fast passage does not glitch, stolen notes do not click, and you know your worst-case callback time.

Ch 08 delay lines · combs · reverb · saturation · plugins

Space & Shipping

One data structure — a circular buffer read at a fractional offset — generates chorus, flanging, delay and reverb. After that, the remaining work is packaging.

The delay line

Allocate a buffer, keep a write index, and read from write − delay, wrapping around. Because the delay is rarely a whole number of samples — and because modulating it is the entire point — the read is interpolated, exactly as in the wavetable of Chapter 3.

Everything below is that one structure with different parameters:

Delay lengthFeedbackModulated?What you call it
0.1–10 msHighYesFlanger
5–30 msNoneYes, gentlyChorus
10–50 msAnyNoComb filter / doubling
50–1500 msAnyNoEcho
Many, in parallelHighNoReverb

Combs, allpasses and Schroeder reverb

A delay with feedback is a comb filter: its response is a series of evenly spaced peaks at multiples of 1/delay, because those frequencies come back in phase every time round the loop. Short delays give widely spaced, audibly pitched combs; that hollow ringing is what a flanger is.

Fig 8.1 — Feedback combs with 2 ms and 10 ms delays. Peak spacing is 1/delay: 500 Hz and 100 Hz. Modulate the delay length slowly and the whole comb slides — the flanger sweep.

An allpass filter passes every frequency at the same level but delays them by different amounts. Musically it does almost nothing on its own, which is exactly why it is useful: it smears a transient into a dense cloud without colouring the spectrum.

Schroeder's 1962 reverb combines them: several parallel combs with mutually prime delay lengths to build echo density without the echoes lining up, followed by a chain of allpasses to diffuse the result. Add a one-pole lowpass inside each comb's feedback path and high frequencies decay faster than low ones — which is what real rooms do, and without it a reverb sounds like a metal tank.

Modern designs use feedback delay networks — a matrix that mixes several delay lines into each other — which give smoother decay for similar cost. Schroeder is where to start; the intuition transfers.

Chorus

Take a short delay, modulate its length with a slow LFO, and mix it back with the dry signal. Because the read pointer is moving, the delayed copy is Doppler-shifted — it detunes slightly against the original, and the beating between them is the effect. Two or three copies at different LFO rates give the lush version. This is also why chorus and vibrato are the same device with different mix settings.

Saturation

A waveshaper maps input amplitude to output amplitude through a nonlinear curve — tanh, a polynomial, a hard clip. Gentle curves add low-order harmonics and read as warmth or glue; hard ones add high-order harmonics and read as distortion.

And, once again: harmonics above Nyquist fold. A hard clipper on a bright signal aliases badly. Oversample the nonlinear stage — upsample 4×, shape, lowpass, decimate — or use a curve chosen to be band-limited. This is the last appearance of the problem introduced in Chapter 0, and by now it should feel inevitable.

J · Effects rack

The Chapter 7 synth, into chorus → delay → reverb
Preset Chorus Dly time Dly fb Dly mix Room Damping Rev mix Cutoff Res Master Panic Transport
Load Pad, raise Rev mix to about 0.5 and hold a chord. Then pull Damping to zero — the tail turns bright and metallic, which is what a room without absorption would actually sound like. Push Dly fb past 0.8 with a short Dly time and you leave echo territory and enter comb-filter territory, which is Fig 8.1 arriving by ear.

Shipping it

A standalone binary with cpal is a fine end point. If you want it in a DAW, the current answer in Rust is nih-plug, which targets CLAP and VST3 from one codebase and handles parameter declaration, automation, state save/restore and GUI hosting. A plugin's process callback has exactly the same constraints as the cpal one, so the DSP you have written ports unchanged — which is the payoff for keeping the audio code free of I/O from the very first chapter.

JobRustPython
Audio I/Ocpalsounddevice
MIDI inputmidirmido + python-rtmidi
WAV read/writehoundwave (stdlib), soundfile
FFT for analysisrustfftnumpy.fft, scipy.signal
Arrays / block mathsplain slicesnumpy
Lock-free queuesringbuf, crossbeamqueue.Queue (locking, but adequate)
DSP to compare againstdasp, fundspscipy.signal, pyo
Plotting while you learnwrite a WAV, open Audacitymatplotlib
CLAP / VST3 pluginsnih-plugnot really — port it
GUIegui, viziadearpygui, tkinter

Python is the better place to understand a filter: you can plot its impulse response in three lines and iterate in seconds. Rust is the better place to ship one. Plenty of people prototype in the first and port to the second, and that is a perfectly respectable way to work through this book.

Checkpoint 08

Your synth loads in a DAW, holds its state, responds to automation, and sounds like something you would actually use.


Where to read next

Vadim Zavalishin, The Art of VA Filter Design — free, and the definitive treatment of the zero-delay-feedback material in Chapter 5. Read it after you have a working biquad, not before. Julius O. Smith's CCRMA books — Introduction to Digital Filters and Mathematics of the DFT, free online, rigorous and complete. Steven W. Smith, The Scientist and Engineer's Guide to DSP — free, and the gentlest on-ramp to Fourier and filters that exists. Will Pirkle, Designing Software Synthesizer Plugins in C++ — C++, but the DSP translates directly and the architectural advice is sound. Sound on Sound's Synth Secrets — around sixty articles, almost no mathematics, unmatched for intuition. Välimäki & Huovilainen, "Oscillator and Filter Algorithms for Virtual Analog Synthesis" — the paper behind Chapter 3. And musicdsp.org for the accumulated folklore.

Ch 09 the capstone · sequencing in the callback · reading a patch

The Instrument

Nine chapters of parts, assembled. Nothing new is introduced here — this is the same code you have been reading, wired together and given something to play.

What you have actually built

Every stage below exists because of a specific problem in a specific chapter. Read the chain left to right and the book reads back at you:

StageFromThe problem it solves
Phase accumulatorCh 01Producing a steady pitch without drift or clicks
PolyBLEP oscillatorsCh 03A bright waveform that does not alias on high notes
Sub and noiseCh 02–03Weight underneath, and air on top
TPT state-variable filterCh 05Removing harmonics, sweepable without thumping
Filter envelopeCh 04–05The downward sweep that makes a note feel struck
Amplitude envelopeCh 04Turning a continuous tone into a note
Key trackingCh 05High notes staying as bright as low ones
LFO and mod routingCh 06Movement, so nothing sits perfectly still
Voice allocationCh 07Playing more than one note without clicks
Chorus, delay, reverbCh 08Placing the sound somewhere rather than nowhere
Step sequencerCh 09Something to play, with timing you can trust

Why the sequencer lives in the audio callback

It is tempting to drive a sequencer from a timer on the UI thread: wake up every so often, decide it is time for the next step, send a note-on. This is how a great many hobby synths do it, and it is why they feel slightly loose.

The arithmetic is unforgiving. A sixteenth note at 120 BPM is 125 ms. If your events can only land on block boundaries, and a block is 128 samples, then every note is quantised to the nearest 2.67 ms — and a browser or OS timer is far worse than that, typically drifting by 5 to 15 ms under load. At that point the sequencer is audibly behind itself, and worse, inconsistently behind itself, which the ear reads as sloppiness rather than lateness.

The fix is to count samples instead of milliseconds. The audio callback already knows exactly how much time has passed, because it is handed exactly n samples' worth. So keep a countdown in samples, and when it runs out mid-block, split the block: render up to the step boundary, fire the note, render the rest. Steps then land on the exact sample they should, forever, with no drift.

samples per sixteenth note step = (fs × 60 / BPM) / 4

At 48 kHz and 120 BPM that is 6,000 samples exactly. Swing lengthens the on-beat sixteenth and shortens the off-beat one by the same amount, so the bar still lasts precisely as long — the groove moves, the tempo does not.

A sequencer inside the audio callback
pub struct Seq {
    notes: [i16; 16],      // -1 means rest
    accent: [bool; 16],
    step: usize,
    to_next: f32,          // samples until the next step
    gate_left: f32,        // samples until this note releases
    sounding: Option<u8>,
    fs: f32, bpm: f32, swing: f32, gate: f32,
}

impl Seq {
    fn step_samples(&self) -> f32 { (self.fs * 60.0 / self.bpm) / 4.0 }

    fn advance(&mut self, synth: &mut Synth) {
        self.step = (self.step + 1) % 16;
        // Swing: on-beat sixteenths get longer, off-beat ones shorter
        // by the same amount, so the bar length never changes.
        let sw = self.swing.clamp(0.0, 0.66) * 0.5;
        let dur = self.step_samples()
                * if self.step % 2 == 0 { 1.0 + sw } else { 1.0 - sw };
        self.to_next = dur;

        let n = self.notes[self.step];
        if n >= 0 {
            if let Some(prev) = self.sounding.take() { synth.note_off(prev); }
            let vel = if self.accent[self.step] { 1.0 } else { 0.6 };
            synth.note_on(n as u8, vel);
            self.sounding = Some(n as u8);
            self.gate_left = dur * self.gate;
        }
    }

    /// Split the block at every event. This is what buys you
    /// sample-accurate timing instead of block-quantised timing.
    pub fn render(&mut self, synth: &mut Synth, out: &mut [f32]) {
        let mut i = 0;
        while i < out.len() {
            if self.to_next <= 0.0 { self.advance(synth); }

            let mut next = self.to_next;
            if self.gate_left > 0.0 { next = next.min(self.gate_left); }
            let chunk = (out.len() - i).min(next.ceil() as usize).max(1);

            synth.render(&mut out[i..i + chunk]);
            i += chunk;
            self.to_next -= chunk as f32;

            if self.gate_left > 0.0 {
                self.gate_left -= chunk as f32;
                if self.gate_left <= 0.0 {
                    if let Some(n) = self.sounding.take() { synth.note_off(n); }
                }
            }
        }
    }
}

The panel below runs exactly this, in an AudioWorklet. The playhead you see stepping across the grid is the audio thread reporting its own position back, not a UI timer guessing.

K · The instrument

Sequencer · 8 voices · 2 osc + sub + noise · filter · 2 envelopes · LFO · 3 effects
Sequencer Pattern Tempo Swing Gate Octave Patch Transport
Osc 1 Osc 2 Osc mix Detune Sub Noise Filter Cutoff Res Env → fc Filt D Keytrack Amp D Amp S Amp R LFO rate → Cutoff Chorus Delay Dly time Reverb Master Panic
Press Engine off to start audio, then set the sequencer to Run. Click any cell in the grid to place or clear a note — you can edit while it plays. The acc row underneath adds accent, which raises velocity, which opens the filter further on that step; that is the whole trick behind an acid line. The diagram at the top dims the stages that are currently bypassed, so you can see the signal path change as you turn things up.

Reading a patch

Four recipes, each expressed as a chain of decisions rather than a list of numbers. Load the pattern, load the patch, then change one thing at a time and listen for what it does.

SoundThe decisions
Acid bass Ladder filter for the saturation. Low cutoff, high resonance, a big filter envelope with a short decay. Accent raises velocity, which opens the filter further — so accented steps squelch and the rest do not. Gate short, so notes separate.
Pluck Sustain at zero on both envelopes: the note is entirely attack and decay. High key tracking so the top of the keyboard stays bright. A little delay for space.
Pad Slow attack on both envelopes, two saws detuned by 10 or so cents, a slow LFO on cutoff, and plenty of reverb. Detune is what makes it wide; without it, two saws in unison are just a louder saw.
Lead Osc 2 up a fifth, moderate resonance, a touch of LFO on pitch for vibrato. Turn the sequencer off and play it from the keyboard.
Three experiments worth doing

One. Load Acid, run it, and slowly sweep Cutoff while raising Res toward 0.9. That is Chapter 5 doing the single most recognisable thing in electronic music.

Two. Set Gate above 1.0 so each note overruns the next. Watch the voice meters: notes now overlap, and the allocator from Chapter 7 starts handing out second and third voices for a line you thought was monophonic.

Three. Turn Swing to 0.4 and count along. The bar still takes exactly as long — check it against the tempo — but the off-beats arrive late. Groove is arithmetic.

What was deliberately left out

This synth is complete enough to be useful and small enough to hold in your head. The obvious next steps, roughly in order of how much they improve the sound per hour spent:

  • Unison and supersaw. Seven detuned saws per voice instead of two, with stereo spread. The cheapest large improvement in perceived quality there is.
  • Oversampling the nonlinear stages. The ladder's saturator aliases, as Chapter 5 warned. Run it at 4× and filter, and the filter stops sounding fizzy when driven.
  • A real modulation matrix. Chapter 6 described one; this synth has fixed routings. Sources × destinations × depth, evaluated per block.
  • Sample-accurate MIDI. The same block-splitting the sequencer does, applied to incoming MIDI events.
  • Stereo. Everything here is mono. Pan per voice, stereo effects, and a widener are mostly plumbing but transform the result.
  • Patch save and load. Dull, and the first thing you will want once you find a sound you like.
  • MPE. Per-note pitch and pressure, if you have a controller that sends it.
Checkpoint 09

You have an instrument. It plays in time, it does not click, it does not alias, and every knob on it is something you could rewrite from scratch. The remaining work is taste.

Ref attribution · provenance · further reading

References

Almost nothing in this book is new. What follows is an attempt to name the people who actually worked it out.

A note on what this list is

This book was written from knowledge of the field rather than assembled from sources open on a desk, and no text, figure or code was copied from any of the works below. So this is not a record of documents consulted while drafting — it is an attribution of ideas to the people who originated them, and a reading list for going deeper. Bibliographic details for the principal algorithmic sources were checked against publisher and primary records; the general reading list at the end is offered as recommendation rather than citation.

The author of this document is Claude Opus 5, an AI model made by Anthropic, writing in August 2026. That is relevant to provenance in both directions: nothing below was transcribed or paraphrased from a source, and equally, nothing below was re-checked against one while the prose was being written. Treat the technical claims as you would treat any confident secondary account — useful, and worth verifying at the primary source before you rely on it.

Where a specific published algorithm is implemented here more or less as its author formulated it, that is stated explicitly in the entry. Two cases matter most: the state-variable filter in Chapter 5 follows Andrew Simper's trapezoidal formulation closely, and the PolyBLEP correction in Chapter 3 is Välimäki and Huovilainen's closed-form polynomial. Everything else is an ordinary implementation of standard, long-published technique.

Foundations · sampling, Fourier, the DFT

  • Harry Nyquist · Claude E. Shannon "Certain Topics in Telegraph Transmission Theory" (Transactions of the AIEE, 1928) and "Communication in the Presence of Noise" (Proceedings of the IRE, 1949). Independently anticipated by Vladimir Kotelnikov (1933) and E. T. Whittaker (1915). Ch 0 — the sampling theorem and the Nyquist limit
  • Joseph Fourier Théorie analytique de la chaleur, 1822. The claim that a periodic function decomposes into a sum of sinusoids. Ch 2 — the entire chapter
  • Henry Wilbraham · J. Willard Gibbs The overshoot at a discontinuity in a truncated Fourier series, described by Wilbraham in 1848 and independently by Gibbs in 1899. Fig 2.2 — Gibbs phenomenon
  • James W. Cooley & John W. Tukey "An Algorithm for the Machine Calculation of Complex Fourier Series", Mathematics of Computation 19(90):297–301, 1965. Anticipated in unpublished work by Gauss around 1805. Every spectrum on this page — the radix-2 FFT is a direct implementation
  • Julius von Hann · Ralph Beebe Blackman & John Tukey The raised-cosine window, named for Hann and popularised through Blackman and Tukey's 1958 work on spectral estimation. Ch 2 — windowing before every FFT here

Oscillators and anti-aliasing

  • Tim Stilson & Julius O. Smith III "Alias-Free Digital Synthesis of Classic Analog Waveforms", Proc. ICMC, Hong Kong, 1996, pp. 332–335. The foundational BLIT and BLEP paper. Ch 3 — the idea that a naive waveform differs from its band-limited ideal only near the discontinuity
  • Eli Brandt "Hard Sync Without Aliasing", Proc. ICMC, Havana, 2001, pp. 365–368. Introduced MinBLEP — the minimum-phase band-limited step, which removes the lookahead latency the symmetric BLEP requires. Ch 3 — the BLEP lineage, and the hard-sync discussion
  • Vesa Välimäki & Antti Huovilainen "Antialiasing Oscillators in Subtractive Synthesis", IEEE Signal Processing Magazine 24(2):116–125, March 2007. This is where PolyBLEP is introduced and named: a closed-form polynomial standing in for a table lookup. Ch 3 — the poly_blep function here is this algorithm
  • Vesa Välimäki & Antti Huovilainen "Oscillator and Filter Algorithms for Virtual Analog Synthesis", Computer Music Journal 30(2):19–31, Summer 2006. Ch 3 and Ch 5 — the survey this book's framing follows
  • Vesa Välimäki "Discrete-Time Synthesis of the Sawtooth Waveform with Reduced Aliasing", IEEE Signal Processing Letters 12(3):214–217, 2005. The DPW method — a fourth approach this book mentions only in passing. Ch 3 — further reading
  • Robert Bristow-Johnson "Wavetable Synthesis 101, A Fundamental Perspective", AES 101st Convention, 1996. The per-octave mipmapped table strategy. Ch 3 — the Wavetable class

Filters

  • Robert A. Moog US Patent 3,475,623, "Electronic High-Pass and Low-Pass Filters Employing the Base to Emitter Diode Resistance of Bipolar Transistors". Filed 10 October 1966, issued 28 October 1969. The transistor ladder. Ch 5 — the circuit the Ladder class approximates
  • Hal Chamberlin Musical Applications of Microprocessors, Hayden, 1980. The digital state-variable filter with a unit delay in the feedback loop — the design whose limitations motivated everything that came after. Ch 5 — the "old solution" the chapter describes and then improves on
  • Robert Bristow-Johnson "Audio EQ Cookbook". Originally circulated as Audio-EQ-Cookbook.txt on the music-dsp mailing list; adapted with permission as a W3C Working Group Note (2021). Closed-form biquad coefficients for every common response. Ch 5 — the Biquad.set() coefficients are these formulae
  • Vadim Zavalishin The Art of VA Filter Design. Written at Native Instruments and distributed free in successive revisions. The definitive treatment of topology-preserving transforms and zero-delay feedback. Ch 5 — the whole delay-free-loop argument
  • Andrew Simper · Cytomic "Solving the Continuous SVF Equations Using Trapezoidal Integration and Equivalent Currents", November 2013. Ch 5 — the SVF class implements this formulation directly: the g/k/a1/a2/a3 coefficients and the two-integrator update are Simper's
  • Ross Bencina · Aaron Wishnick · Jean Laroche Bencina's partial time-varying BIBO stability analysis of Simper's SVF (music-dsp mailing list, 2013), completed in Wishnick's "Time-Varying Filters for Musical Applications", Proc. DAFx-14, Erlangen, using Laroche's state-space method. Ch 5 — the claim that the TPT SVF stays stable under fast cutoff modulation is their result, not folklore
  • Antti Huovilainen "Nonlinear Digital Implementation of the Moog Ladder Filter", Proc. DAFx-04, Naples, 2004, pp. 61–64. Nonlinearities placed inside the one-pole stages rather than only in the feedback path. Ch 5 — the saturation discussion; this book uses the simpler single-saturator form
  • Tim Stilson & Julius O. Smith III "Analyzing the Moog VCF with Considerations for Digital Implementation", CCRMA, Stanford. Ch 5 — the structure of the digital ladder
  • Julius O. Smith III Introduction to Digital Filters with Audio Applications, CCRMA / W3K Publishing. Free online, and the reason this book can state the pole-zero geometry in a paragraph. Ch 5 — difference equations, the z-transform, Fig 5.2
  • Alan V. Oppenheim & Ronald W. Schafer Discrete-Time Signal Processing, Prentice Hall. The standard academic reference for LTI systems, convolution and the z-transform. Ch 5 — background

Envelopes, MIDI and perception

  • Vladimir Ussachevsky The four-stage attack/decay/sustain/release envelope is conventionally credited to a suggestion Ussachevsky made to Robert Moog in the mid-1960s. Ch 4 — the ADSR shape
  • Jean-Claude Risset & David Wessel "Exploration of Timbre by Analysis and Synthesis", in Diana Deutsch (ed.), The Psychology of Music, Academic Press. Includes the classic demonstrations that removing the attack transient makes instruments hard to identify. Ch 4 — "why attack transients carry identity"
  • MIDI Manufacturers Association MIDI 1.0 Detailed Specification. Including the rule that note-on with velocity 0 is a note-off. Ch 4 — the message parsing in the Rust listing

Modulation

  • John M. Chowning "The Synthesis of Complex Audio Spectra by Means of Frequency Modulation", Journal of the Audio Engineering Society 21(7):526–534, September 1973. Ch 6 — essentially the entire chapter: carrier/modulator ratios, the modulation index, and Bessel-function sideband amplitudes
  • John Chowning & David Bristow FM Theory and Applications: By Musicians for Musicians, Yamaha Music Foundation, 1986. The practical companion to the 1973 paper. Ch 6 — the ratio table and the electric-piano recipe
  • Friedrich Wilhelm Bessel Bessel functions of the first kind, Jk, which give the sideband amplitudes — including the zero of J₀ near index 2.4 at which the carrier vanishes. Ch 6 — Fig 6.1

Real-time practice

  • Ross Bencina "Real-time audio programming 101: time waits for nothing", 2011, rossbencina.com. On blocking, worst-case complexity, allocation, locks, page faults and garbage collection in an audio callback. Ch 1 and Ch 7 — the rules list is his framing, including the emphasis on worst case over average case
  • George Marsaglia "Xorshift RNGs", Journal of Statistical Software 8(14):1–6, 2003. The Osc.white() noise source — a 32-bit xorshift, chosen because it allocates nothing and branches never

Delay, reverberation and effects

  • Manfred R. Schroeder & Benjamin F. Logan "'Colorless' Artificial Reverberation", IRE Transactions on Audio 9(6):209–214, 1961. The allpass reverberator. Ch 8 — the Allpass class
  • Manfred R. Schroeder "Natural Sounding Artificial Reverberation", Journal of the Audio Engineering Society 10(3):219–223, July 1962. Parallel combs into series allpasses. Ch 8 — the Reverb class is this topology
  • "Jezar" at Dreampoint Freeverb, released to the public domain in the late 1990s. Not used as source, but its comb and allpass tunings established the delay-length conventions this book's reverb sits in the lineage of. Ch 8 — the mutually-prime delay lengths
  • Jean-Marc Jot & Antoine Chaigne "Digital Delay Networks for Designing Artificial Reverberators", AES 90th Convention, 1991. Feedback delay networks, building on Stautner and Puckette (1982). Ch 8 — the "modern designs" this book points to but does not implement
  • Julius O. Smith III Physical Audio Signal Processing, CCRMA / W3K Publishing. Free online; the reference treatment of delay lines, fractional interpolation and comb structures. Ch 8 — the Delay and Comb classes

General reading recommended in the text

  • Steven W. Smith The Scientist and Engineer's Guide to Digital Signal Processing, California Technical Publishing. Free at dspguide.com. Recommended in Ch 8 as the gentlest on-ramp
  • Will Pirkle Designing Software Synthesizer Plugins in C++, Focal Press. Recommended in Ch 8
  • Gordon Reid "Synth Secrets", Sound on Sound, 1999–2004. Around sixty articles, almost no mathematics, unmatched for intuition. Recommended in Ch 8
  • Charles Dodge & Thomas A. Jerse Computer Music: Synthesis, Composition and Performance, Schirmer. Recommended in the study plan
  • Curtis Roads The Computer Music Tutorial, MIT Press. The encyclopedic survey. General background
  • musicdsp.org · the music-dsp mailing list Decades of accumulated practitioner folklore, contributed by too many people to name. Much of the working knowledge in this book has no other citable origin. Throughout — denormal handling, DC blockers, smoothing constants, voice stealing heuristics

Software and type used to build this page

  • W3C Audio Working Group The Web Audio API, and specifically AudioWorklet, which is what makes it possible to run hand-written per-sample DSP on the browser's audio thread rather than settling for built-in nodes. Every sound on this page
  • The Rust audio ecosystem Named in Ch 8: cpal and midir (RustAudio community), hound (Ruud van Asseldonk), rustfft, ringbuf, dasp, fundsp (Sami Perttu), nih-plug (Robbert van der Helm) and egui (Emil Ernerfeldt). Ch 8 — the tooling table. See the note below regarding nih-plug.
  • Typefaces Barlow Condensed (Jeremy Tribby), Spectral (Production Type) and IBM Plex Mono (Mike Abbink with Bold Monday, for IBM). All under the SIL Open Font License, served via Google Fonts. Panel legends, body text, and all data readouts respectively
One correction to Chapter 8

Chapter 8 recommends nih-plug for building CLAP and VST3 plugins. Since that text was written, the RustAudio community has forked it as nice-plug, a community-led project that now describes itself as the recommended toolkit for Rust audio plugin development. nih-plug remains a reasonable starting point and the API is closely related, but check the current state of both before committing.

On the code in this book

The DSP source embedded in this page was written for it. The two exceptions are noted above: the state-variable filter follows Simper's published formulation, and PolyBLEP is Välimäki and Huovilainen's polynomial. Both are published algorithms intended to be implemented. If you spot anything else here that belongs to someone and is not credited, that is an error worth correcting.