Introduction

TapTools is a collection of Max/MSP objects with roots back to 1999, rebuilt in 2026 on a portable DSP kernel library (this repository) with thin Max wrappers (the TapTools-Max package). This book is its field guide, in the tradition of the AmbiTap and SampleRateTap books and MuTap's Quieting the Loop: one chapter per object family, written for the person patching at 11 pm, not for the person grading a DSP exam.

Each chapter makes the same promises:

  • It says what the thing is for — and, near the end, when it is the wrong tool, because every tool is sometimes the wrong tool.
  • Every performance claim is measured, not remembered. The numbers in these chapters come from the kernel's own test suite and from the executed verification notebooks in notebooks/, which drive the same C++ code the Max objects compile through a C ABI. When a chapter says "47 dB", a notebook cell measured 47 dB, and you can re-run it.
  • Trade-offs are stated as trades. Knobs that buy something always pay with something; the chapters try to name both sides.

The book is organized the way a patch is:

  • Part I — Sources: the virtual-analog oscillator, tap.vco~, including its analog-character section and the honest Moog recipe.
  • Part II — Filters: the morphing Simper SVF (tap.svf~), the transistor ladder (tap.ladder~), and the envelope filter modeled on the Snow White AutoWah (tap.autowah~) — with its hardware-calibration harness.
  • Part III — Strings, rooms, and spirals: exact true-stereo convolution (tap.convolve~) and the two GRM Tools recreations — the tuned comb bank (tap.5comb~) and the pitch-accumulating shimmer loop (tap.pitchaccum~).
  • Part IV — The spectral set: the 24-band vocoder (tap.vocoder~), the per-bin spectral gate (tap.nr~), and the bin remapper (tap.spectra~).
  • Part V — The rhythm section: the Roland recreations — the TB-303 voice, its diode-ladder filter, and its sequencer (tap.303~, tap.diode~, tap.303.seq~), and the eight TR-808 voice channels with their row sequencer (tap.808.*, tap.808.seq~).
  • Part VI — Staying in tune: the pitch corrector (tap.tune~) and the detection/resynthesis machinery it stands on.
  • Part VII — The pedalboard: the stompbox recreations — the voiced feedback overdrive (tap.overdrive~), chasing the TS-lineage feedback pedals rather than a waveshaping curve.
  • Part VIII — The machine, file by file: the SampleRateTap-style deep dives — one chapter per kernel header, deriving the math, reviewing the code, and recording why each algorithm is written the way it is, alternatives and all. Parts I–VII are for driving the objects; Part VIII is for trusting them — or changing them.
  • Part IX — Recipes: whole patches chasing specific sounds — the TR-808 kits behind four decades of records, the three-oscillator Moog voice — with settings you can check against the reference pages and the honest accounting of what each ingredient buys.

More chapters land as objects mature; the utility and Jitter objects live in their reference pages, where they belong.

The oscillator and its knobs

Play a perfectly calculated sawtooth for ten seconds and you will learn something uncomfortable: perfection sounds like a diagram. Every cycle identical, every harmonic exactly where the textbook puts it, nothing moving — the ear files it under test tone and stops listening. Now play three of them, each a few cents off the others and each wandering a little, into a filter that pushes back — and the same arithmetic becomes a synthesizer. This chapter is about tap.vco~: what it generates, what each attribute trades, and how to get from the diagram to the instrument — including the honest version of the Moog recipe.

Companion material: the object's reference page (docs/tap.vco~.maxref.xml) and help patcher (help/tap.vco~.maxhelp in the TapTools-Max package) wire up every control in this chapter; the verification notebook shows every number quoted here as an executed, plotted measurement.

One phase, four shapes, no aliasing panic

Inside the object there is a single master phase ramping from 0 to 1 at the frequency you asked for. Everything else is a way of reading that phase: a sine reads it through sin, a saw stretches it to ±1, a pulse compares it to the pulse width, and the triangle integrates the pulse (the classic analog trick, reproduced digitally because it behaves so well). The continuous shape parameter (0 sine → 1 triangle → 2 saw → 3 pulse) crossfades adjacent readings of the same phase, so a shape sweep glides through hybrid waveforms without resetting anything.

The digital oscillator's ancient enemy is aliasing: a naive saw's harmonics march past Nyquist and fold back as inharmonic garbage. tap.vco~ suppresses this with polyBLEP — each waveform discontinuity is rounded across ±1 sample by a polynomial that closely matches what a band-limited step would do. Measured against a naive saw at 3951 Hz (a B7, ugly on purpose): the 13th harmonic folds back to 3.4 kHz, where the naive saw puts it at −27 dB and tap.vco~ puts it at −74 dB — 47 dB of alias suppression right where the ear is most offended.

Two things about this are worth knowing so they don't surprise you:

  • The waveforms look "not band-limited" on a scope. Expected. The BLEP correction touches two samples per edge — at 440 Hz that's 2 of ~109 samples per cycle — and there is none of the Gibbs ripple that brickwall band-limited waves show, because nothing is truncated. The shape stays essentially ideal; the spectrum is what's controlled.
  • Alias suppression is not alias elimination. Push the fundamental into the kilohertz range and distant fold-backs remain, tens of dB down. For melodic and bass registers they are simply gone.

The wiring

   frequency (signal or float)     FM, in Hz (signal)     sync (signal)
              |                          |                     |
        +-----+--------------------------+---------------------+-----+
        |                       tap.vco~                              |
        +-----------------------------+-------------------------------+
                                      |
                              (signal) the waveform
  • Inlet 1 sets the frequency — a float sets the attribute, a signal drives it with true per-sample resolution.
  • Inlet 2 is through-zero linear FM, calibrated in Hz: the input adds directly to the effective frequency. Drive it past the carrier and the phase genuinely runs backward (that's the "through zero" — the classic DX-style sideband sound stays coherent instead of collapsing). Measured: a 500 Hz sine carrier under ±900 Hz of FM stays bounded at exactly 1.0 peak and puts its sidebands where the textbook says.
  • Inlet 3 is hard sync: every rising zero crossing of the input resets the phase, with sub-sample accuracy and an alias correction on the reset. Measured: a 187 Hz slave synced to a 110 Hz master emerges periodic at 110.1 Hz — the pitch follows the master, the timbre follows the slave's frequency, which is the whole trick of sync sweeps.

Single-channel, like every TapTools DSP object: wrap it in mc. for stacks, and keep reading, because the analog section was designed around exactly that.

Signal-flow diagram of the VCO: pitch, FM, and the per-seed analog section sum into a master phase accumulator, which fans out to four waveform readings crossfaded by shape

One phase, many readings — with polyBLEP correcting the edges and the analog section injecting in exactly two places.

The knobs, one by one

frequency, and gliding

Hz, from LFO rates (0.01 Hz) to 20 kHz. Every parameter in the object rides a per-sample ramp whose length is the smooth attribute (ms, default 20) — and on frequency that ramp is portamento. Set smooth to 60–100 ms, send note frequencies as floats, and you have the Minimoog glide, no extra objects. For stepped pitch, set smooth low; for per-sample modulation, use the signal inlet (which bypasses smoothing entirely — you are the smoothing).

shape and waveform

shape is the continuous morph; the waveform sine|triangle|saw|pulse message snaps it to a corner. The corners are the pure shapes; everything between is a crossfade of neighbors on the shared phase. Slow shape sweeps are an underrated modulation destination — the morph is click-free by construction (measured: a 2-second sweep from 0 to 3 keeps its RMS within a factor of ~5 and never drops out).

pw — pulse width

Percent, 1–99, audible as shape approaches 3. The calibration is exact: a bipolar pulse at duty d must average 2d−1, and the measured means at 10/25/50 % are −0.800/−0.500/+0.000. PWM by an LFO into pw (via messages, riding the smooth ramp) is the cheapest "two oscillators" impression one oscillator can give.

gain, presets, interp

gain is output level in dB. Sixteen preset slots store every parameter (store 1store 16), and recall morphs to a slot over interp milliseconds (or an explicit time: recall 3 4000) — every parameter riding its ramp simultaneously, shape included. A preset morph across two very different voicings is a patch element in its own right.

seed — which unit you own

Everything random in this oscillator — the drift walk, the jitter noise, and (below) the component tolerances — is generated deterministically from seed. Same seed, same render, bit for bit: your mixes reproduce and the test suite can pin behavior exactly. Different seeds decorrelate. The mental model that pays off: a seed is a serial number. One tap.vco~ with seed 7 is a particular oscillator that came off the line; seed 8 is the unit next to it in the crate. An mc. stack with per-voice seeds is a set of instruments, not copies of one.

The analog section

Here is why a hardware oscillator sounds alive, reduced to what a DSP model can honestly act on. A real VCO is unstable at two time scales — it wanders over seconds (thermal drift) and trembles over milliseconds (noise in the core) — it is mis-calibrated in a structured way (the V/oct converter is exact at its trim point and increasingly wrong away from it), and its waveforms carry the circuit's fingerprints (a bowed ramp, a rounded reset corner, a duty cycle that isn't quite 50 %). None of these is large. All of them are always present, all slightly different from unit to unit, and the ear reads their sum as alive long before it can name any of them.

tap.vco~ models each one with its own control, all in real units, all deterministic per seed, and all exactly zero by default — the default object is the ideal oscillator, and the kernel's test suite pins that at imperfect 0 every seed renders bit-identically.

drift — the slow wander (cents)

A random walk: sample-and-hold noise at ~2 Hz smoothed through a ~0.5 Hz one-pole, scaled to the depth you set. This is the thermal story — the pitch center strolling around over seconds. In a unison stack it is the difference between "chorus effect" and "three players": chorus modulation is periodic and shared; drift is aperiodic and per-voice. Ranges: 3–8 cents reads as a well-serviced vintage instrument; 15–25 as a charming one; 50+ as a broken one.

jitter — the fast tremble (cents)

New with this chapter: the short-time companion — noise at ~80 Hz through a ~40 Hz smoother, so the pitch trembles cycle-to-cycle instead of strolling. Measured at 10 cents depth: the relative spread of individual periods is 2.7×10⁻³ (a few cents, exactly as labeled), against 2×10⁻⁷ for the ideal oscillator — four orders of magnitude more micro-instability, still nothing like vibrato. This is the control that stops a sustained single oscillator from sounding frozen. Ranges: 1–4 cents is felt more than heard; 8–15 is audible grit on pure waveforms.

detune and track — the calibration story (cents, cents/octave)

detune is the static offset — the coarse fact that oscillator 2 was never exactly oscillator 1. track is subtler and very analog: cents of error per octave from A440, the exponential converter drifting from its trim point. Measured with track 5: exactly 0.0 cents at A440, +15.0 cents three octaves up, −15.0 three octaves down, a clean line through the middle. Solo it is nearly invisible; in a stack played across the keyboard it is why vintage unisons get wider — and slightly wilder — up the neck. Ranges: real serviced hardware tracks within 1–3 cents/octave; ±5 is a synth that needs its yearly appointment.

imperfect — the circuit's fingerprints (0..1)

One knob for the waveform-shape story, scaled by per-seed component tolerances so each seed misbehaves in its own direction:

  • the saw ramp bows into the familiar shark-fin (a visible, scope-obvious shape change; spectrally it is mostly a phase effect — stated here so you don't chase magnitude changes that aren't there),
  • the saw's reset corner rounds off: a gentle one-pole closing from ~22 kHz toward ~8 kHz — measured 6.4 dB down at the 40th harmonic (17.6 kHz) at full imperfection; extreme top-end air, traded for warmth,
  • the triangle goes asymmetric, and this one is very audible in the spectrum: the ideal triangle's 2nd harmonic sits at −185 dB (i.e., absent); at imperfect 0.8 it rises to −34 dB relative to the fundamental — even harmonics, the classic "warm" giveaway,
  • the sine picks up mild waveshaper color, and the pulse width takes a small static offset (so two "50 %" pulses from two seeds beat against each other the way two real units do),
  • the whole unit takes a static pitch offset of up to a couple of cents.

Ranges: 0.2–0.4 is a healthy vintage unit; 0.6–0.8 is character you can point to in a mix; 1.0 is a unit with a story. At 0, every seed is the same ideal machine — the analog section never costs you the reference oscillator.

The performance section

Where the analog section models what the circuit does on its own, these controls model what a hand does to it — added after the Recipes chapters had to teach a scaling formula to get constant-width vibrato out of the Hz-calibrated FM inlet.

  • vibrato / vibrato_rate — a sine LFO on the pitch, depth in cents (0–100) and rate in Hz, so ten cents is ten cents in every register. Measured: at a commanded ±100 cents the peak cycle-to-cycle deviation reads 90–110 cents, and the modulation crosses its mean at exactly twice the commanded rate (pinned by test).
  • vibrato_delay — the singing control: the vibrato fades in through a one-pole with this time constant (ms), re-armed on every new note (every frequency-target change), so held notes bloom and passing notes stay plain. Pinned: early deviation under 60 % of settled, and shallow again right after a note change. The signal-rate frequency inlet deliberately does not re-arm — there, you are the modulation.
  • bend — pitch bend in semitones (±24), riding the standard smooth ramp: the wheel, as an attribute. Pinned within 5 cents of the commanded interval.

All of it is deterministic with no randomness — and at depth 0 the output is bit-identical to the ideal oscillator (pinned), so the reference instrument is still free.

The Moog recipe, honestly

The sound everyone wants from this object is three oscillators into a ladder. Here is the recipe, with the honest accounting of which ingredient does what. Rendered A/B demos of exactly this patch (through the real kernels) live in the notebook material.

voicefrequencydetunedriftseed
1f−4 c8 c11
2f+5 c8 c22
3f ÷ 2+2 c10 c33
  • All three: @shape 2 (saw), @smooth 70 for glide, @jitter 3, @track 2, @imperfect 0.3.
  • Sum them (scale by ~1/2.8) and feed tap.ladder~: @mode lp24 @resonance 0.35 @drive 9 @asym 0.45 @comp 0.25.

What each ingredient buys, in order of importance:

  1. The stack itself. Three free-running voices at ±cents is most of the sound. The beating between them is the fatness; the octave-down third voice is the weight. (tap.vco~ free-runs like hardware — no per-note phase reset — so the beat pattern is different on every note. That, not any single voice's tone, is the big analog tell.)
  2. The ladder. drive into the tanh stages compresses and colors the stack; asym adds the even harmonics of mismatched transistors; comp kept low preserves the authentic passband droop as resonance rises. A perfect saw into a driven asymmetric ladder sounds more "Moog" than an imperfect saw into a clean one — spend your character budget here first.
  3. Glide. 60–100 ms of smooth on the note changes. Iconic, and free.
  4. The analog section. Drift keeps the beating from ever repeating; jitter un-freezes sustains; per-seed tolerances make the three voices three units. This is seasoning — essential in the way salt is, invisible in the way salt is.

Omit in reverse order when CPU or taste says so.

When it is not the right tool

  • You need an exact test signal. Actually — it is the right tool: imperfect 0 (the default) is the mathematically ideal oscillator, and the test suite holds it there. Just don't reach for the analog section and a measurement mic in the same patch.
  • You want evolving spectra from one voice — wavetables, granular motion, additive drift. This oscillator's spectrum is fixed per shape by design; morph shape or FM it, but a wavetable oscillator is a different instrument.
  • You want chorus. Twenty cents of drift on one voice is not a chorus; it is a seasick oscillator. Chorus is a delay effect — use one.
  • Noise. The bottom of the shape range is a sine, not a noise source; tap.noise~ has five colors of the real thing.

Checkpoint

One master phase, four shapes and their hybrids, polyBLEP keeping the folded harmonics ~47 dB down. Frequency glides on smooth, FM is in honest Hz and survives through zero, sync locks pitch to the master while timbre stays yours. The analog section is four controls with real units — slow drift, fast jitter, structured mis-calibration in track, circuit fingerprints in imperfect — all scaled by per-seed component tolerances, all exactly off by default, all deterministic: a seed is a serial number. And the Moog recipe is mostly the stack and the ladder — let the oscillator's imperfections season, not carry.

The filter that morphs

Every synthesizer needs one filter it can trust with anything: a bass line, a noise sweep, a parametric EQ move, an audio-rate modulation stunt. tap.svf~ is that filter — a state-variable design in Andy Simper's trapezoidal (zero-delay-feedback) formulation, the same lineage as the filters in Ableton Live, including Auto Filter's Morph type. This chapter is what each attribute trades, and why the design earns the trust.

Companion material: the reference page and help patcher in the TapTools-Max package, and the verification notebook, where every number below is an executed, plotted measurement.

Why "state-variable," and why this one

A state-variable filter computes all its responses — lowpass, bandpass, highpass, notch — from the same two internal states at once, which is what makes continuous morphing between them possible at all. The classic digital version (Chamberlin) famously misbehaves at high cutoffs and under fast modulation. Simper's TPT formulation fixes both: the tuning is prewarped (exact all the way to Nyquist) and the filter is unconditionally stable under per-sample cutoff modulation — the property that later let this same kernel become the sweep engine inside tap.autowah~. The notebook slams the cutoff across five octaves with a 90 Hz LFO under full-band noise; the output stays bounded, no oversampling tricks required.

Signal-flow diagram of the TPT state-variable core: a summing node into two trapezoidal integrators with damping and low feedback, and the output mix that forms every response

Two integrators in a zero-delay loop; every response — and the morph — is three multiplies downstream of the same two states.

The knobs, one by one

type — the discrete responses, the morph, and the EQ family

Ten responses from one core. The classics — lowpass, highpass, bandpass, notch, peak, allpass — plus:

  • morph: one continuous parameter sweeps LP → BP → HP → notch → LP (0 → 0.25 → 0.5 → 0.75 → 1). The corners are bit-identical to the discrete modes — measured max difference exactly 0 — so morphing to a corner is that filter. A slow morph under a held chord is a patch element the discrete modes can't give you.
  • bell, lowshelf, highshelf: the parametric-EQ trio from Simper's coefficient tables, with a ±24 dB gain. Measured: a +12 dB bell peaks at +12.00 dB; a −9 dB low shelf lands −9.00 dB in its plateau and 0.00 dB on the other side. These always run a single 2nd-order section — cascading would square the boost, so order is ignored for them, on purpose.

order — 2, 4, or 8 poles that stay flat

Orders 2/4/8 (12/24/48 dB per octave) run as a cascade with the Butterworth Q spread, so at resonance 0 the response is maximally flat and sits at −3.01 dB at the cutoff regardless of order — measured −3.01 at every one, with slopes of 12.3/24.7/49.4 dB per octave. The trade against a naive cascade of identical sections (which droops long before fc): none. This is just the correct way to stack poles.

resonance — normalized, and honest about the top

0 to 1: 0 is the Butterworth-flat base, 1 is the edge of self-oscillation. Resonance sharpens only the final section of a cascade, so you get one clean resonant peak on a flat passband instead of a compounding stack of peaks. A q message converts to and from engineering Q if you think in those units.

circuit — clean or driven

  • clean is the pure linear filter: cheapest, transparent, never oversampled. Also the reference: the EQ modes and every measured Bode plot above are this circuit.
  • driven adds drive (dB) into a tanh limiter on each section's band node — an OTA-flavored color stage, oversampled (1/2/4×, default 2×). Two measured consequences: a 200 Hz tone through +18 dB of drive grows odd harmonics that simply do not exist in the clean circuit (the 3rd harmonic appears out of the numerical floor, ~140 dB up), and at resonance 1.0 the filter self-oscillates at the cutoff — measured 999.7 Hz for a 1 kHz setting, amplitude bounded by the saturator. It needs a ping to start: a perfectly silent filter is a fixed point.

frequency, the right inlet, and smooth

Float or attribute sets the cutoff through the anti-zipper ramp (smooth, ms). A signal in the right inlet takes over per sample — that's the path for audio-rate filter FM and for envelope-follower patches. Sixteen preset slots morph via store/recall over interp milliseconds, everything gliding together.

Recipes

  • The synth voice: @type lowpass @order 4 @resonance 0.4, envelope into the frequency inlet. Order 4 is the "synth filter" slope; order 2 is the polite one; order 8 is a wall.
  • The DJ sweep: @type morph, sweep morph 0 → 0.5 while easing frequency — the LP-through-BP-to-HP arc is the whole move in one parameter.
  • Tone control: bell/shelves with modest gains. It measures exact, so trust the numbers you type.
  • A sine with character: @circuit driven @resonance 1, ping it, and tune with frequency — a self-oscillating test-tone-with-a-temper.

When it is not the right tool

  • You want the classic squelchy 4-pole growl. That's a transistor-ladder sound — resonance that compresses the passband, saturation inside the loop. Next chapter: tap.ladder~.
  • You need many static EQ bands. One tap.svf~ per band works, but a dedicated multiband EQ (or tap.filter~, the RBJ multimode biquad) is the boring, correct choice.
  • You want the filter to follow your playing. That's tap.autowah~, which is this filter plus an envelope detector and a sweep law.

Checkpoint

One TPT core, every response as an output mix: discrete modes, a morph whose corners are bit-identical to them, and an exact parametric-EQ trio. Butterworth-spread orders stay −3.01 dB flat at any slope; resonance sharpens only the last section; the driven circuit adds tanh color and true bounded self-oscillation at the cutoff. Unconditionally stable under per-sample modulation — which is why other objects build on it.

The transistor ladder

Some filters are tools; this one is a character actor. The four-stage transistor ladder — the Moog circuit — colors everything it touches: the resonance pushes back against the bass, the stages saturate into one another, and at the top of the resonance range it stops filtering and starts singing. tap.ladder~ is a zero-delay-feedback model of that circuit with a tanh saturator in every stage. This chapter is what each control trades, and what the measurements say the model actually delivers.

Companion material: the reference page and help patcher in the TapTools-Max package, and the verification notebook — every number below is an executed measurement. For the linear ladder — the cheap, polite Stilson/Smith model — see tap.fourpole~; this object is its nonlinear sibling.

What the model gets right

Two things separate a serious ladder model from a filter with a "Moog" label:

  • Tuning that survives the top octaves. The classic digital shortcut goes audibly flat as the cutoff rises. This model is prewarped ZDF: measured self-oscillation lands at 1000.2 Hz for a 1 kHz cutoff (0.02 % error) — and, the part that's actually hard, 8009 Hz for an 8 kHz cutoff (0.11 %). You can play the resonance like an oscillator anywhere on the keyboard.
  • Nonlinearity inside the loop, not bolted on. Each stage saturates, and the feedback fights the saturation the way the hardware does. That is where the compression, the "sag," and the bounded self-oscillation come from.

Signal-flow diagram of the ladder: drive into a summing node, four tanh one-pole stages in series, the resonance feedback tap, the comp compensation path, and the Xpander pole-mix taps

The whole filter: four stages, one loop. The red tap sets resonance, the amber paths are the comp bargain and the Xpander mode taps.

The knobs, one by one

frequency and the right inlet

Cutoff in Hz; a signal in the right inlet drives it with true per-sample resolution. Like everything here it rides the smooth ramp when set by message.

resonance — up to and past the edge

0 to 1.1. At 1.0 the loop gain reaches the oscillation threshold; above it the filter sings at the cutoff, amplitude-limited by the tanh stages (ping it to start — silence is a fixed point). Under the edge, resonance does the authentic ladder thing: it eats your passband (see comp).

drive — how hard to lean on the stages

Input gain (dB) into the saturating ladder. Measured THD on a 100 Hz tone: 0.5 % at 0 dB, 3.5 % at 8, 16.5 % at 16, 33 % at 24 — a smooth walk from "slightly thick" to "fuzz pedal's cousin." All odd harmonics, because tanh is symmetric — which is exactly why asym exists.

asym — the even harmonics of real hardware

Real transistors don't match; their operating points sit slightly off-center, and that asymmetry is where a hardware ladder's even-harmonic warmth lives. asym (0..1) models the mismatch. Measured on a driven tone: the 2nd harmonic sits at −156 dB (numerically absent) at asym 0 and rises to −18.6 dB relative to the fundamental at 0.6. One honest warning from the reference page: an asymmetric saturator can produce slight signal-dependent DC — follow with tap.dcblock~ if something downstream cares.

comp — the passband bargain

A real ladder trades passband level for resonance: the feedback subtracts from the input. Measured at resonance 0.9: the passband sits at −13.2 dB with comp 0 (the authentic droop) and at 0.0 dB with comp 1 (fully restored). Vintage behavior or modern behavior — your call, continuously.

mode — pole mixing, the Xpander trick

lp24, lp12, bp12, bp24, hp12, hp24: mixing the ladder's stage taps yields whole families of responses from the same four poles (the Oberheim Xpander's famous trick). Measured small-signal slopes: 23.4 dB/oct for lp24, 11.7 for lp12. The resonance and saturation behavior carries into every mode — a resonant bp24 through drive is a very different animal from tap.svf~'s clean bandpass.

oversample — paying for the saturation honestly

The tanh stages generate harmonics past Nyquist that fold back as inharmonic alias tones. Measured on a hard-driven 5 kHz tone: going from 1× to 4× oversampling drops the non-harmonic (alias) energy by 13.5 dB. The default 2× is the working compromise; use 4× when you drive high notes hard, 1× when you're filtering bass and counting CPU.

solver — fast or exact

The nonlinear loop can be solved with one predictor-corrector pass (fast, the default) or by Newton iteration to convergence (exact, circuit-simulation accuracy). They are audibly identical until drive and resonance are both pushed hard; exact is there for when you want to know, and for renders where CPU is free.

Recipes

  • The bass patch: tap.vco~ saw stack (see the oscillator chapter's Moog recipe) → @mode lp24 @resonance 0.35 @drive 9 @asym 0.45 @comp 0.25. Keep comp low; the droop is the vintage glue.
  • The acid line: @resonance 0.85 @drive 15, envelope into the frequency inlet, and let the resonance fight the saturation.
  • The kick synthesizer: @resonance 1.05, ping it with a click, and ride frequency down fast — a self-oscillating ladder is a sine with attitude.

When it is not the right tool

  • Transparent filtering. Every pole of this filter has an opinion. For surgical work use tap.svf~ (clean circuit) or tap.filter~.
  • Morphing responses. The pole-mix modes switch; they don't glide. Continuous response morphing is tap.svf~'s morph.
  • CPU-constrained patches that just need "4-pole lowpass." tap.fourpole~ is the linear ladder at a fraction of the cost — no saturation, no oversampling, no opinions.

Checkpoint

A prewarped ZDF four-stage ladder with tanh in every stage: self-oscillation in tune within 0.11 % even at 8 kHz, drive that walks THD from 0.5 % to 33 %, asym switching on the even harmonics of mismatched transistors, comp choosing between authentic passband droop and modern flatness, pole-mixed multimode outputs, and oversampling that measurably pays down the saturation's aliasing. The character filter — spend your tone budget here.

The pedal that listens

A wah pedal is a filter with a foot attached. An auto-wah cuts out the foot: it listens to how hard you play and sweeps the filter for you — hit a string and the filter opens; let it ring and the filter settles back down. tap.autowah~ models a specific, beloved instance of the idea: the Mad Professor Snow White AutoWah, Björn Juhl's OTA-based envelope filter, grounded in the traced circuit and the published behavior. This chapter is how to drive it — and how we will know, measurably, when the model matches the pedal.

Companion material: the reference page and help patcher in the TapTools-Max package; the design document (plans/tap.autowah~.md in TapTools-Max) with the full hardware research; and the validation notebook, which measures everything below and ends with a cell waiting for recordings of the real pedal.

What the hardware is, in one paragraph

A 2-pole state-variable filter (an LM13700 OTA circuit) whose frequency is pushed up from a resting point by an envelope detector — a diode and a capacitor, charged fast, discharged at a rate you set. Four knobs: Sensitivity (how hard your signal drives the sweep), Decay (how fast it falls back), Bias (the resting frequency), Resonance (the Q). Published sweep: 250 Hz to about 2.5 kHz — a throaty, vocal range, deliberately unlike the quack of a Mu-Tron-style filter. One secret feature: with Sensitivity at minimum it becomes a fixed, manually swept filter — the "cocked wah."

The model composes tap.svf~'s Simper core (one 2nd-order section, driven per sample — the modulation stability that filter chapter promised, cashed in) behind a rectifier → attack/release follower and an exponential sweep law. The measured control behavior:

  • Sweep law: cutoff = bias · 2^(sweep · range). Measured against the design curve across the full envelope range: max error 0.000 cents. The law lives in one function on purpose — if the real pedal turns out to sweep linearly in Hz, one function changes and nothing else moves.
  • Timing: attack set to 2 ms measures 1.94 ms; decay set to 250 ms measures 256 ms, and the release fits a pure exponential with residual σ = 0.004 — an RC discharge, like the hardware.

Signal-flow diagram of the autowah: the audio path through the borrowed SVF core and dry/wet mix, with the detector chain of sensitivity gain, rectifier, RC follower, tanh knee, and exponential sweep law driving the cutoff

A detector, a law, and a borrowed filter — the amber chain is everything this object adds to the SVF it composes.

The knobs, one by one

sensitivity — the trigger level, and the secret mode

Detector input gain in dB (−60..+24). Tune it to your instrument and touch: too low and only your hardest hits open the filter; too high and everything pins at the ceiling (a tanh soft knee compresses hard playing into the top rather than slamming a rail). At −60 the envelope is exactly off and the object becomes the cocked wah: a fixed resonant filter with bias as the manual sweep control. Factory preset 4 ships that voicing.

decay — the personality knob

How fast the filter falls back to bias, in ms (10..5000). Fast (tens of ms) gives a wah articulation on every note — the funk setting. Slow (hundreds of ms up) gives classic auto-wah swells that ride your phrasing. This is the knob to perform.

bias and range — where the sweep lives

bias is the resting frequency (default 250 Hz, the hardware's home); range is the sweep span in octaves above it (default 3.3, the hardware's 250 → ~2500 Hz). Both go far beyond the hardware if you want them to, and direction 1 sweeps down from bias instead — a TapTools extension the pedal never had.

resonance, mode, drive

resonance (0..1) is the Q — the vocalness. mode picks the filter tap: lowpass is the stock voicing; bandpass is the circuit's other node, a known hardware mod — quackier and noticeably quieter. drive (dB) engages the saturating SVF circuit for OTA-flavored color; 0 keeps it pure.

attack, mix, and the rectifier

The hardware's attack is fast and fixed; ours defaults to the same 2 ms but is exposed (0.05..100 ms) for softer onsets. mix is an equal-power dry/wet the pedal never had — 100 % (wet-only) is the hardware. And under the hood the detector's rectifier is selectable: full-wave (default, cleaner tracking) or half-wave (the traced single-diode topology). Measured: the half-wave detector carries 2.6 % signal-rate ripple on a low tone against the full-wave's 0.7 % — a real, quantified flavor difference awaiting the hardware A/B.

The sidechain inlet and the envelope outlet

A signal in the right inlet takes over the detector: one sound wahs another (a kick opening a pad is the classic). The right outlet emits the envelope (0..1) as a signal — the detector as a free modulation source for anything else in the patch.

Factory voicings

Preset slots 1–4 ship guitar, bass (lower bias, tighter range — the GB pedal's instrument switch, as a preset you can morph to), slow swell, and cocked wah. recall 2 4000 morphing from guitar to bass over four seconds is its own effect.

How we'll know it matches

The validation harness is built and proven on ground truth. An STFT peak-trajectory extractor recovers the swept resonant peak from wet audio alone — no dry reference needed — and against the kernel's own cutoff trace it correlates at 0.979 in log-frequency, with the small measured offset (−37 cents) close to what resonant-peak physics predicts (−20 cents at that Q). The same code runs on demo videos and on the real pedal. A Snow White is on order; when it arrives, reamped recordings drop into notebooks/reference/ and the notebook's last cell overlays hardware against model. Disagreements map one-to-one onto kernel constants. That pass may flip the default filter tap or the sweep law — both are flagged, isolated, and waiting.

When it is not the right tool

  • You want the filter on a knob or LFO, not your dynamics. That's tap.svf~ with a signal in its frequency inlet — this object's own core, without the detector.
  • Your source has no dynamics. A static pad through an auto-wah is a static filter. Feed the sidechain something rhythmic instead.
  • You want the Mu-Tron quack. Different circuit, different voicing — raise resonance, try mode 1, but know you're modding a Snow White, not summoning a Mu-Tron.

Checkpoint

An envelope detector with a fast attack and a musician's decay knob, driving a 2-pole resonant filter up from bias through an exponential law that measures exact to the design. Sensitivity at the floor is the cocked wah; the sidechain inlet and envelope outlet make the detector patchable; the factory slots hold the four voicings that matter. And the model doesn't ask to be trusted — the extractor that will judge it against the real pedal is already built, already proven, and already waiting in the notebook.

Borrowed rooms

Every room you have ever heard is a filter: clap once, and what comes back — the impulse response — is everything the room will ever do to any sound. Convolution reverb plays your signal through that recording. tap.convolve~ does it in true stereo against an impulse response held in a buffer~, using the standard engine of the genre (uniformly-partitioned overlap-save FFT convolution), and its defining property is worth stating up front: it is exact. Not "high quality" — exact. This chapter is what that buys, what it costs, and how to drive the two knobs that actually matter.

Companion material: the reference page and help patcher in the TapTools-Max package, and the verification notebook — the first notebook in this repo, and the template for all the others.

Exact, measured

The engine splits the IR into blocksize-sample partitions, transforms each once, and multiply-accumulates in the frequency domain over a delay line of past input spectra. The bookkeeping is intricate; the result is not. Measured against a direct time-domain convolution of the same float32 IR: maximum difference 3×10⁻¹² — double-precision noise. Change the block size and the output doesn't change either (64/256/1024 agree within 2×10⁻¹², latency-removed). An impulse through the engine reconstructs the IR to 5.5×10⁻¹⁴, and a synthetic 0.60 s-RT60 reverb measures back at 0.599 s. There is no "character" in this engine to audition; the character is entirely in the IR you load.

The wiring, and the one real cost

Stereo in, stereo out, IR from a named buffer~. The cost: latency of exactly blocksize samples — verified for every block size — on top of your I/O latency. That is the entire quality/latency dial:

  • blocksize small (64–128): tight enough for live input; more CPU per sample (more, smaller FFT batches).
  • blocksize large (512–2048): cheapest; latency grows to match. For a send/return reverb on a mix bus, nobody hears 21 ms of pre-delay you didn't ask for — except you, so use predelay deliberately instead.

maxsize reserves capacity (both are locked in while DSP runs and apply on restart). The rest of the surface mirrors tap.verb~ so the two reverbs read as siblings: mix, gain, predelay, normalize (energy-based, so quiet and hot IRs land at comparable levels), bypass, mute.

Diagram of uniformly partitioned overlap-save convolution: framed input blocks through an FFT into the frequency-domain delay line, multiplied per bin against the double-buffered IR partition spectra, then IFFT with the aliased half discarded

The wiring: one FFT in, one IFFT out, and a multiply-accumulate that is the only cost growing with IR length.

True stereo, by channel count

A stereo room isn't two mono rooms: sound from the left source arrives at the right ear too. The engine runs the full 2×2 matrix — LL, LR, RL, RR — and the buffer~'s channel count selects the topology:

  • 4+ channels: true stereo, all four paths (measured: a signal sent only left emerges on the right at exactly the cross-feed path's gain, 0.600 expected, 0.600 measured, with zero leakage where paths are silent).
  • 2 channels: dual mono — L and R convolved separately, no cross-feed.
  • 1 channel: the same mono room on both sides.

Loading rooms while the music plays

IRs are analysed off the audio thread and published atomically into a double-buffered slot: swapping IRs mid-performance neither clicks nor drops (measured RMS across the swap instant: 21.9 before, 22.1 just after), and one block later the output is bit-identical to an engine that had the new IR from the start. Load rooms like presets; the engine doesn't flinch.

Recipes

  • The honest room: a measured IR (church, plate, spring — the internet is full of them), @mix 25 @normalize 1, and resist the urge to EQ the IR itself before trying predelay — 10–30 ms of it buys clarity for free.
  • Not a reverb at all: an IR is any filter. A single click is a delay; a strummed guitar body is a body simulator; a vowel is a formant filter. Convolution doesn't know it's supposed to make reverb.
  • True-stereo width: record or synthesize the four paths with a genuinely different LR/RL from LL/RR — the cross-feed is where "being in the room" lives.

When it is not the right tool

  • You want to design the reverb — decay knobs, damping, modulation, gated tails. A static IR can't do any of that; tap.verb~ (the algorithmic Moorer reverb, with its own oversampling and limiter) can.
  • Zero-latency insert on a live path. The engine costs blocksize samples, full stop. At 64 that's 1.3 ms — small, not zero.
  • Time-varying convolution. Swaps are click-free but discrete; the engine doesn't interpolate between rooms.

Checkpoint

Partitioned convolution is exact linear convolution — measured to 10⁻¹² — with one honest cost, blocksize samples of latency, and one honest dial, block size against CPU. True stereo comes from the buffer's channel count; IR swaps are atomic and dropout-free; and everything the effect sounds like is the impulse response you feed it. Borrow better rooms.

Five strings, no guitar

Feed a comb filter its own output and it stops being an EQ curiosity and becomes a string: a resonator with a pitch, a ring time, and a temperament. Five of them, tuned by hand, is an instrument — that was the insight of the GRM Tools Classic "Comb Filters" plugin, and tap.5comb~ is its recreation: five resonant combs with per-voice tuning, masters that play the whole bank, and the preset-morph engine that made the GRM tools feel alive. This chapter is how to tune, ring, and morph it.

Companion material: the reference page and help patcher in the TapTools-Max package, and the grm_comb_render tool in the kernel repo, which renders the listening-check scenarios outside Max.

What a resonant comb actually is

A delay of 1/f seconds fed back on itself resonates at f and all its harmonics — pluck it with noise and it rings like a string tuned to f. Two implementation details decide whether five of them sound like a chorus of strings or like a broken flanger, and both were the reasons this object was recreated rather than ported:

  • Fractional delays. At 48 kHz, a 440 Hz comb needs a delay of 109.09 samples. Round it to 109 and the comb plays 440.37 Hz — every voice lands on a slightly wrong, slightly different wrong pitch, and the beating between voices (the whole point of a bank) is gone. The delays here are Hermite-interpolated: the tuning is continuous, and sweeps glide instead of zippering.
  • No clipper in the loop. The feedback path uses a DC blocker and a precise feedback cap, not a hard limiter — high resonance rings clean instead of distorting.

Signal-flow diagram of one comb voice: input sums with feedback into a fractional delay line, read by Hermite interpolation, with the feedback ring running through the loop lowpass, normalized DC blocker, warp allpass, and ring-time-derived feedback gain; a pickup tap at half the loop feeds the output subtractor

One voice of five. The red ring is the string; the amber tap is the pluck position.

The knobs, one by one

freq1..5 and freq — the tuning

Per-voice frequencies (5 Hz floor, the GRM's own) — or the notes message, which tunes up to five combs from MIDI note numbers in one list (fractional allowed) — plus a master multiplier (0..2) that transposes the whole bank — the master is the performance control, gliding every voice proportionally so chords stay chords.

res1..5 and res — ring time, not feedback

Resonance is mapped to ring time on a log curve, 20 ms to 100 s, and the feedback coefficient is derived from the current delay — so a voice keeps its ring time as its pitch sweeps, instead of ringing longer at low notes and choking at high ones (the raw-feedback behavior of naive combs, and of the legacy abstraction). 50 is a decaying pluck; 80+ sustains; near 100 it is a drone that outlives your patience.

lp1..5 and lp — the string's brightness

A one-pole lowpass inside each feedback loop: every pass around the loop gets darker, which is exactly how real strings decay (highs first). Open it for metallic; close it toward a few hundred Hz for felt and thump.

warp — stiff strings

New to this recreation: a negative-coefficient allpass in the loop disperses the partials — upper harmonics round-trip faster and stretch sharp, the inharmonicity of a stiff piano string. The main tap is compensated at each voice's fundamental, so the pitch stays put while the timbre goes piano-ish, then bell-ish. At extreme warp × high tuning the loop can't get shorter than the dispersion and the pitch flattens — physical, and documented.

phase — where you pluck the string

Also new: a half-loop pickup tap. At 100 the even harmonics cancel — the sound of plucking a string exactly at its midpoint. Neutral at 0.

gain, mix, and the morph engine

Equal-power dry/wet and output gain, plus the GRM hallmark: sixteen preset slots with timed interpolation. store 1, retune everything, store 2, then recall 1 8000 — every frequency, resonance, and damping glides for eight seconds through territory you never explicitly tuned. Grabbing one parameter mid-morph overrides just that parameter. The morph is not a transition between sounds; it is the sound.

Recipes

  • The resonator chord: tune freq1..5 to a voicing (say 80/120/160/200/ 102 Hz — the legacy factory preset), res high, and feed it drums or speech. The input is now an excitation signal for your chord.
  • The piano that isn't: moderate resonance, warp 40, lp around 3 kHz, and pluck with clicks.
  • The eight-second gesture: two stored extremes and a long recall — the classic GRM move. Automate nothing else.

When it is not the right tool

  • One comb, precise and plain: tap.comb~ is the single, cheaper unit.
  • Echoes rather than pitch: delays long enough to hear as repeats are tap.delay~ / tap.multitap~ territory — a comb is a delay, but this one is tuned and normalized for resonance, not slapback.
  • Faithful nostalgia: this deliberately is not the legacy tap.5comb~ abstraction — the integer delays, linear feedback, and in-loop clipper it had are exactly what was retired, and the deviations are flagged in the reference page for the audition.

Checkpoint

Five Hermite-tuned resonant combs with ring time on a log map (20 ms–100 s), per-loop damping, and two ways to bend the string physics (warp for stiffness, phase for pluck position) — under masters that transpose the bank and a sixteen-slot morph engine that turns retuning into performance. Strings, chords, drones, and gestures; no guitar required.

The spiral staircase

Most pitch shifters are a one-way trip: in, transposed, out. The GRM Tools "PitchAccum" closed the loop — the transposed signal is delayed and fed back into the transposer, so every pass around the loop shifts it again. Set +7 semitones and a note becomes a rising spiral: +7, then +14, then +21, each echo climbing, the whole thing dissolving upward like light on water. That loop is the effect everyone now calls shimmer, years before the name. tap.pitchaccum~ is the recreation: two independent transposer-delay loops ("shadows") with the accumulation wired in. This chapter is how to climb.

Companion material: the reference page and help patcher in the TapTools-Max package, and the grm_pitchaccum_render tool in the kernel repo for listening checks outside Max.

The loop, and why it doesn't collapse

Each shadow is: granular transposer (±24 semitones) → delay (up to 3 s) → feedback → back into the transposer. Three design choices keep the spiral musical instead of muddy:

  • Constant-level grains. The transposer sweeps two taps half a cycle apart, each windowed so the pair sums exactly to 1 at every phase and every crossfade width. The original tt_shift engine's window pair didn't quite sum flat, which imposed an amplitude ripple at the grain rate — after ten trips around a feedback loop, ripple compounds into tremolo. Here the tenth pass is as steady as the first.
  • Hermite-interpolated taps. Fractional delays keep each pass in tune, so the spiral's steps are the interval you set, not the interval plus drift.
  • A capped, DC-blocked loop. Feedback tops out at 0.99 with a DC blocker in the path — the spiral can run for a very long time, but it is unconditionally bounded (unit-tested at the cap).

The signature is measurable: set +7 semitones and the kernel test finds energy at +7 and +14 — the second pass, the accumulation itself.

Two signal-flow diagrams contrasted: tap.pitchaccum~ feeds its output back into the delay buffer upstream of the moving transposer taps, so every pass is transposed again; the ordinary shifter-in-a-feedback-loop patch taps the delay output and only ever shifts once

The topology is the effect. Feedback re-enters upstream of the taps, so the staircase climbs; in the ordinary patch every echo is the same interval.

The knobs, one by one (per shadow, ×2)

trans1 / trans2 — the step of the staircase

±24 semitones, continuous. Musical intervals (+7, +12, +5) make harmony; small offsets (±0.1–0.3 st) make lush detune-echo instead of a spiral; negative values descend into the dark version nobody expects.

delay1 / delay2 — the tread depth

Up to 3 s per shadow. Short (50–150 ms) blurs the passes into a texture; long (0.5–2 s) articulates each step of the climb as an audible echo.

fb1 / fb2 — how many steps

How much survives each trip, 0–99. 30 gives two or three audible generations; 70 a long climb; 90+ a texture that essentially sustains until the transposition walks it out of range (energy shifted past the audible band is the spiral's natural exit).

xfade — the grain crossfade

GRM's Cross-fade control: the width of the grain envelope's flanks. Narrow is more articulate and more grain-rate flavored; wide is smoother and softer in attack. Because the envelope pair always sums to 1, this changes texture, never level.

The modulation section, and follow

A global LFO (with modphase offsetting shadow 2, so the two loops breathe against each other) plus per-voice deterministic random transposition modulation — a little of either keeps long spirals from sounding cloned. follow (off by default) engages a pitch follower — decimated normalized autocorrelation, confidence-gated, deliberately picking the smallest plausible lag so it doesn't lock onto subharmonics — which adapts the grain window toward the detected period: cleaner transposition on monophonic sources, ignored gracefully on noise.

Presets

The sixteen-slot morph engine, as everywhere in the GRM pair: two stored spirals and a timed recall between them is a gesture in itself.

Recipes

  • Shimmer, the classic: shadow 1 at +12, delay ~400 ms, fb1 75; shadow 2 at +7, delay ~650 ms, fb2 60; both into a reverb (tap.convolve~ with a long church, or tap.verb~). The reverb is load-bearing — shimmer is spiral plus wash. The full patch has its own recipe in Part IX.
  • The descent: −5 and −12, long delays, moderate feedback — a staircase into the basement, much rarer and much creepier.
  • Micro-thickener: ±0.15 st, 60/90 ms delays, feedback 50, xfade wide — not a spiral at all, just an expensive-sounding widener.

When it is not the right tool

  • One clean transposition, no loop: tap.shift~ is the plain shifter — same modernized engine, none of the plumbing.
  • Formant-true vocal shifting: granular transposition shifts formants with the pitch; chipmunks live this way. tap.harmony~ is the dedicated tool — formant-preserving voices at fixed intervals, chords included.
  • Rhythmically exact multi-tap echoes: the delays here serve the loop; tap.multitap~ serves the grid.

Checkpoint

Two transposer-delay loops where the feedback re-enters the transposer, so pitch accumulates pass after pass — +7 becomes +14 becomes +21. Constant-sum grain envelopes keep the tenth pass as steady as the first; Hermite taps keep it in tune; the capped, DC-blocked loop keeps it bounded forever. Intervals are the architecture, delay is the pacing, feedback is the height — and the morph engine turns the whole staircase into something you can bend mid-climb.

The tape that forgets slowly

Every other delay in this house is kept honest by a cap: feedback stops just short of one, because a loop that gains nothing and loses nothing will pile up until it clips. tap.discreet~ is built on the opposite bargain. Its regeneration goes all the way to 1.0 — legally, cleanly, forever — because the loop forgets: every pass through the tape comes back a little darker and a little softer than it went in. The memory loss is not a defect the kernel tolerates; it is the mechanism that keeps the machine stable. You are not patching a delay effect. You are renting a machine whose memory is the instrument.

The rig it recreates is printed on the back cover of Discreet Music (Obscure/EG, 1975): Brian Eno's synthesizer feeding one Revox tape machine, the tape spooling for seconds across the room to a second machine, and the second machine's playback both sent to the speakers and folded back into the first machine's record head. It is the same two-machine system Robert Fripp ran for the No Pussyfooting loops. The tape path itself — the fractional read, the periodic wow and flutter, the in-loop coloration — follows the published tape-echo modeling literature (Arnardóttir, Abel, and Smith's AES model of the Echoplex, and Välimäki et al.'s tape-echo work). The schematic is the score; this kernel is a faithful performance of it.

Companion material: the executed notebook discreet.ipynb, which measured every claim below, and the eno_render tool, whose discreet_basic and discreet_sustain scenarios are the listening copies. The Max wrapper lands in the TapTools-Max package alongside the rest of the family.

Signal-flow diagram of tap.discreet~: input through a send-level fader and record head onto seconds of tape, a wow/flutter-modulated play head, an equal-power dry/wet mix out, and a red return path of darkening lowpass, bounded saturation, DC blocker, and regeneration gain back into the record head

Two machines and a spool of tape; the red return is where the forgetting — and therefore the stability — lives.

loop — the tape span

loop_seconds is the distance between the machines: how long a phrase travels before it returns. The kernel test pins the grid to the sample — an impulse comes back at exactly one loop, bit-for-bit the first time, and every later return lands within a sample of its grid point.

Changing the loop while audio runs is a tape-speed change, not a menu option: the read head physically glides to its new distance, and gliding a read head is doppler. Move from 0.5 s to 0.75 s over half a second and the playback drops an octave while the transport re-spools, then re-locks on pitch — the test measures 220 Hz mid-glide and 440 Hz within five cents after. There is no crossfading "digital" mode, on purpose. If a pitch bend on loop changes would ruin the patch, this is the wrong delay (see below).

regen is the return level into the record head, and unlike tap.delay~'s feedback (capped at 0.99), it reaches exactly 1.0. The notebook plays a one-second noise burst into the loop at regen 1.0 and lets it run for twenty seconds: the level settles and stays — no growth, no collapse — because the wear path bounds it. The saturator's output can never exceed 1/drive regardless of what the loop accumulates, the DC blocker keeps offsets from stacking, and the darkening lowpass decides what survives: lows sustain, highs surrender. The pinned scenario is blunt about the contract — it asserts non-growth, never decay, because at regen 1.0 sustain is the promise. Bring regen down, or darken harder, to end a piece; clear is the eject button, and regen-1.0 material is gone for good.

darken and drive — the wear

darken_hz is the record/playback corner: every pass through the loop runs through a one-pole lowpass at this frequency, so a bright phrase sheds its treble generation by generation while its body lingers. This is measured, not vibes: with the corner at 2 kHz, a 6 kHz tone loses to 0.292 of itself per pass and a 300 Hz tone keeps 0.890 — and both numbers match the analytic transfer of the wear path to three decimals in the executed notebook.

Per-pass level of a 300 Hz and a 6 kHz tone recirculating through the loop, measured points landing on the analytic prediction lines

Generation loss, measured against regen · |H_wear|. The tape forgets treble first.

drive is the record-head saturation — the guarantee. At any drive above zero the loop is absolutely bounded no matter the settings; at drive 0 the path is exactly linear (a real bit-for-bit passthrough, not "almost") and the loop leans on darkening alone. Drive around 0.5 is the tape sound; drive high is the loop slowly compressing itself into a wash.

wow and flutter — the transport

Two sines, slow-deep and fast-shallow, breathing the play head's position. The pitch math is honest and checkable: depth times 2π times rate is the peak deviation, so 2 ms of wow at 0.5 Hz predicts ±10.9 cents — and the notebook's YIN pitch track measures 10.9. The transport is periodic and deterministic by design (no stochastic capstan drift): two renders of the same settings are bit-identical, which is also a pinned test. Set both depths to 0 for a perfectly still machine.

input_level — the performance move

The fader Eno actually rode was not the output — it was the send. Play a few phrases into the machine, then bring input_level to zero: the loop keeps unrolling everything it holds, worn a shade further every pass, and the piece continues without you. That gesture — set up a system, feed it, step away — is the whole record, and it is one setter here. mix is the ordinary equal-power dry/wet with bitwise-exact endpoints.

Recipes

  • The Discreet Music bed: @loop 5. @regen 0.95 @darken 3500 @drive 0.4 @mix 60. Play sparse, slow phrases; stop; listen to what the tape decides to keep.
  • Frippertronics: @loop 6.5 @regen 1. @drive 0.7 @darken 2200 @mix 100. Solo over yourself from a minute ago. The wash never clips and never ends until you end it.
  • Haunted slapback: @loop 0.15 @regen 0.85 @wow 4. 0.9 @flutter 0.15 12. — a short loop with a seasick transport; the doppler and the wear turn a slap delay into a memory of one.
  • The exit: whatever is running, ride @regen from 1. to 0.7 over a minute. The piece performs its own fade, oldest material first.

When it is not the right tool

  • Rhythmic delays. Loop changes bend pitch by design, and there is no tempo sync. tap.delay~ is the clean line; tap.multitap~ is the pattern.
  • Anything that must not color the repeats. Wear is always in the loop (drive 0 removes only the saturation, not the darkening you set). If the tenth echo must equal the first, this machine is philosophically opposed.
  • Loops that should line up with other loops. One machine, one spool. For a bank of independent free-running loops, the next chapter's tap.airport~ is the instrument.

Checkpoint

Seconds of tape between two machines; a worn return path — darken, saturate, DC-block — instead of a feedback cap; regeneration to exactly 1.0 because forgetting is the stabilizer. Loop moves are honest tape-speed doppler, the transport is two deterministic sines measured in cents, and the send fader is the performance. Every number above lives twice: as an executed cell in discreet.ipynb and as a pinned scenario in tests/discreet_test.cpp, which CI runs on every push.

Loops that never line up

Take seven tape loops of deliberately awkward lengths — none a multiple of another — put one soft phrase on each, and let them all turn at once. Each loop is trivial: it plays the same thing forever. The system is not: the phrases drift against each other, meet, part, and meet again differently, and the pattern of coincidences does not repeat within a human afternoon. That is "2/1" from Brian Eno's Music for Airports (Ambient 1, EG, 1978), as he described the rig in the album's liner notes and in A Year with Swollen Appendices: the lengths are the score, and the machine's whole job is to keep the loops turning without an opinion. tap.airport~ is that machine — up to eight free-running loops, each with a single head that both plays and records, summed to stereo.

The discipline that makes it the instrument it is: nothing resets a phase. Not recording, not a level move, not a pan, not even a length change. The free-run is the composition, and the kernel treats the heads as sacred; the test suite literally hammers every setter mid-run and then checks that the heads have advanced by exactly the samples processed.

Companion material: the executed notebook airport.ipynb, which measured every claim below, and the eno_render tool's airport_two_one scenario — three stereo minutes of seven loops, the listening copy. The Max wrapper lands in the TapTools-Max package alongside the rest of the family.

Signal-flow diagram of tap.airport~: one tape loop of eight drawn as a circle with a single play-and-record head, input through a record gate, playback through darken, level, and equal-power pan into stereo sums, with the other loops ghosted behind

One loop of eight. The head plays, then records, then advances; nobody ever tells it where to be.

Record and return

record(loop, 1) punches the input onto that loop's tape at wherever its head happens to be — there is no downbeat, no quantized punch-in, because Eno's rig had none. Recording replaces (each phrase was recorded once, not overdubbed), and playback reads just ahead of the write, so while recording you hear the previous generation under the head. record(loop, 0) freezes the tape, and freezes it bit-exactly: the pinned test compares two whole passes of a frozen loop and requires them identical to the bit. A loop is not a degrading medium here — it replays the same magnetic imprint every revolution, which is why this kernel deliberately has no per-pass generation loss (that is tap.discreet~'s physics, not a loop's).

The lengths are the score

length_seconds per loop is where the composing happens. Two loops of 24000 and 30000 samples realign only at their least common multiple — 120000 samples, 2.5 seconds — and the kernel will tell you: composite_period_seconds reports exactly 2.5 for that pair, confirmed in the notebook by rendering the coincidence raster and watching it repeat at 2.5 s and at no shorter lag.

Return raster of two incommensurate loops and their sum, with the 2.5-second composite period marked

Two awkward lengths and their coincidences. Stretch the lengths and the composite period leaves the room.

Then stretch toward the piece: give seven loops airport-scale lengths in awkward ratios and the composite period overflows a 64-bit sample count — the kernel reports infinity, which is not a failure mode. It is the point.

Changing a length while running is a splice: the tape keeps its content and the head re-wraps modulo the new length — never rewinding — exactly as cutting a physical loop shorter would land you mid-phrase. It can click. Splices do.

Level, pan, shade

Each loop has a slewed linear level, an equal-power pan with exact endpoints (a hard-panned loop is bitwise absent from the far bus — the same law as tap.multitap~), and a darken corner that shades that loop's playback tone. The shade is a static one-pole per loop, not wear: measured in the notebook, a 6 kHz phrase through a 1 kHz shade lands at 0.169 of its transparent twin, against an analytic prediction of 0.169. At the band ceiling — the default — the shade stage is bypassed entirely and playback is bit-transparent, which is what makes the freeze and hard-pan promises testable as bitwise facts rather than tolerances.

There is deliberately no wow here: the phasing engine of "2/1" is the incommensurate lengths, not pitch drift. If a loop's source should breathe like tape, run it through tap.discreet~ on the way in.

Recipes

  • The terminal: seven loops, @lengths 17.8 19.1 21.3 23.9 26.2 28.7 30.9, one sustained tone phrase recorded onto each, levels around 0.45, pans spread wide, a 4 kHz shade on two of them. Let it run. Come back in an hour; it will not have repeated.
  • Phase study: two loops, lengths in a near ratio (say 8.0 and 8.1), the same short phrase on both, panned hard left and right — the Reich-adjacent version, where the drift itself is the melody.
  • Sound-on-sound sketchpad: one loop, @lengths 12., record gate on a footswitch. Punch in fragments as they occur to you; the head's indifference to your downbeat is the charm.
  • Breathing loops: patch sources through tap.discreet~ (gentle wow, regen 0) before the record gate — tape transport on the way in, stable free-run once captured.

When it is not the right tool

  • Synchronized looping. This machine never lines up by design. A beat-locked looper wants a phase reset on the downbeat, which is the one thing this kernel refuses to do.
  • Degrading loops. A frozen loop here is bit-eternal. For material that should wear out as it circulates, tap.discreet~ is the machine with the forgetting built in.
  • Dense delay textures. Eight long loops is a composition system, not an echo; tap.multitap~ does a hundred taps without ceremony.

Checkpoint

Up to eight free-running loops, one sacred head each: record replaces at wherever the head is, freeze is bitwise, splices re-wrap and never rewind, and no setter touches a phase. Level, exact-endpoint pan, and a bypassable playback shade place the phrases; the lengths do the composing, and composite_period_seconds tells you how long until the piece repeats — ideally, longer than you will be alive. Every number above lives twice: as an executed cell in airport.ipynb and as a pinned scenario in tests/airport_test.cpp, which CI runs on every push.

The garden that plays itself

The first two chapters of this part recirculate sound: tape that forgets, loops that never agree. This one recirculates decisions. Plant a note and it comes back every pass of the loop a step quieter and a step purer, until it fades below hearing and retires. Plant several and they braid. Stop planting altogether and, after a patient interval, the garden starts planting for itself — always on the scale, never in a hurry. You do not play this instrument so much as tend it, which is exactly the posture Eno kept asking for: the composer as gardener, not architect. The kernel is named for that metaphor.

What it recreates is the principle behind Brian Eno and Peter Chilvers' generative apps (Bloom, 2008), as described in their published interviews and in Eno's 1996 "Generative Music" talk: touch becomes note, note repeats and fades, scale makes wrong notes impossible, idleness hands the piece to the system. The principle only — no scale tables, timings, or sounds are taken from the app, and its name is a live trademark of Opal Limited, which is why this object is a garden and not a bloom. (As with tap.tune~'s history paragraph, none of this is legal advice; the project's ship-gate is a freedom-to-operate review.)

Companion material: the executed notebook garden.ipynb, which measured every claim below, and the eno_render tool's garden_played and garden_idle scenarios, the listening copies. The Max wrapper lands in the TapTools-Max package alongside the rest of the family.

Signal-flow diagram of tap.garden~: notes through a scale quantizer into a 64-event ring, fired at their loop positions into a 16-voice FM bell pool, with a red per-pass path multiplying velocity by decay and brightness by soften back into the ring, and a dashed seeded gardener planting into the ring

Events on a loop instead of audio on a tape — the same recirculation, one level of abstraction up.

Plant and return

note(pitch, velocity) plants: the pitch snaps to the current root and scale at entry, a soft two-operator FM bell sounds on the next sample, and the event takes a seat at the loop's current position. Every pass, it fires again at velocity × decay, and below floor it retires. The notebook's staircase is the whole contract in one figure: a plant at 0.8 with decay 0.5 returns at 0.795, 0.399, 0.2, 0.1, 0.05 — then silence, and active_events reads zero.

A rendered waveform showing five returns of one planted note, each half the height of the last, with the measured peak levels labeled and the retirement floor marked

The return staircase: decay 0.5, floor 0.05, and a bloom that knows when it is finished.

That arithmetic is also the stability story. The family's inversion — degradation as the stabilizer — reaches its third form here: a bloom lives exactly ceil(log(floor/velocity) / log(decay)) passes, so the population of live events converges by construction no matter how fast you plant. And beneath the arithmetic sits a hard bound: sixteen bells in a fixed pool, the quietest stolen when a seventeenth is needed, its envelope re-aimed rather than reset so a steal glides instead of clicking.

soften — returns get purer, not just quieter

Each pass also multiplies the event's brightness by soften, and brightness is the bell's FM index: the upper partial fades while the fundamental holds, so a bloom collapses toward a sine as it recedes — the tape chapters' generation loss, restated in partials instead of passbands. The notebook measures the sideband-to-fundamental ratio shrinking every single return, and the pinned test requires it strictly.

The scale contract

root and scale (chromatic, major, minor, and both pentatonics — plain public-domain scale theory) define where plants may land, and quantization happens at entry: the notebook plants all thirteen chromatic pitches from 60 to 72 into a C major-pentatonic garden and the YIN oracle reads every sounded note on {C, D, E, G, A}. Wrong notes are not discouraged; they are unrepresentable, which is most of why instruments in this family feel effortless to strangers. Because quantization is at entry, changing the scale re-pitches nothing already planted — the field changes for future seeds only.

The gardener

idle_seconds is the patience: that long after your last plant, the garden begins seeding itself, roughly one note per loop pass, uniformly placed, on the scale, within two octaves. The randomness is the family's seeded xorshift64* with the full tr808 contract, pinned as a triad: same seed, bit-identical garden; different seed, a different garden; gardener disabled (idle_seconds 0), the seed cannot matter at all, because the generator is never consumed. This is the library's first randomized event source — step_seq.h proudly promises "no randomness anywhere" — and the seed contract is what lets a generative instrument live in a test suite that demands reproducibility.

Recipes

  • The lobby: defaults, @idle 30. @level 0.4, plant four or five notes, walk away. The garden holds the room indefinitely, bounded.
  • The music box: @decay 0.5 @soften 0.7 @idle 0 @bell 0.005 0.8 1. — no gardener, fast decay: each phrase you play unwinds itself to silence in a few passes, a wind-up toy running down.
  • The endless install: @scale minorpentatonic @root 2 @idle 3. @seed 2008 @level 0.35, never touch it again. Same seed next year, same garden.
  • Duet: @idle 6. and stay at the keyboard — every silence longer than six seconds, the gardener answers you; every plant of yours resets its patience.

When it is not the right tool

  • Melodies with wrong notes in them. Quantization is always on; chromatic passing tones survive only in @scale chromatic, and micro-tonal pitches not at all. This is a fence, and it is the product.
  • Rhythm. Events return on the loop grid, exactly, forever — no swing, no humanization. For patterns as rhythm, tap.808.seq~ is the machine.
  • Any other timbre. One soft bell family, on purpose. It is an instrument, not a polysynth; for FM as a playground, patch oscillators.

Checkpoint

Notes become events; events recirculate on a loop, quieter by decay and purer by soften each pass, retiring below floor; a sixteen-bell pool bounds the sound and a sixty-four-seat ring bounds the score, oldest bloom yielding first. The scale makes wrong notes unrepresentable, and a seeded gardener keeps the piece alive exactly as long as you neglect it. Every number above lives twice: as an executed cell in garden.ipynb and as a pinned scenario in tests/garden_test.cpp, which CI runs on every push.

Making the machine talk

The vocoder is audio's oldest identity theft: take the shape of one sound and wear it over the body of another. Speech works because your mouth sculpts a moving spectral envelope; a vocoder measures that envelope on one signal (the modulator — usually a voice) and stamps it onto another (the carrier — usually a synth), and the synth talks. tap.vocoder~ is the classic architecture: a 24-band channel vocoder, time-domain, no FFT. This chapter is how to wire it and — mostly — how to choose the two signals, which is nine tenths of vocoding.

Companion material: the reference page and help patcher in the TapTools-Max package; the kernel's Catch suite pins the structural behavior quoted below.

The machine, in one pass

Two identical banks of 24 bandpass filters, log-spaced from 50 Hz to 12 kHz (RBJ constant-peak biquads — unconditionally stable across the range). The modulator goes through one bank; a per-band envelope follower measures each band's level. The carrier goes through the other bank; each carrier band is multiplied by the matching modulator envelope; the bands are summed. That's the whole machine — which is why its behavior is so predictable:

  • A silent carrier is silence, no matter what the modulator does (pinned by test): the modulator only ever gates; every sample you hear is carrier.
  • Gain is exactly linear (pinned): the vocoder adds no nonlinearity of its own.
  • A silent modulator decays to silence at the follower rate — the vocoder "lets go" of the carrier the way the voice lets go of a word.

The wiring

Modulator in the left inlet, carrier in the right. Getting these backwards is the classic first-patch bug, and it sounds like it: a synth "speaking" your voice is right; your voice weakly filtered by a synth is backwards.

Signal-flow diagram of the vocoder: the modulator through a 24-band filter bank into envelope followers, the carrier through an identical bank, per-band multipliers, and a summed gain stage

Two identical banks meeting at 24 multipliers. Envelopes gate the carrier; modulator audio never reaches the output.

The knobs, one by one

q — intelligibility vs. smoothness

The bandwidth of all 48 filters. Narrow (high q) separates the bands cleanly — crisper consonant detail, more "robot" — but thins the carrier between band centers. Wide (low q) overlaps the bands into a smoother, duller blend. The classic hardware vocoders sat toward smooth; intelligibility came from performance, not q.

response_interval — how fast the mouth moves

The envelope followers' period in ms. Short tracks every consonant — crisp, maximally intelligible, and a little nervous. Long smears syllables into pads — the "choir" setting. This knob is the vocoder's attack and release; 20–50 ms speaks, 200+ ms sings.

gain

Makeup level, since a band-multiplied signal usually lands quieter than either input. Linear, boring, necessary.

sibilance — the built-in s and t budget

The classic channel-vocoder unvoiced path (Dudley's lineage): a seeded internal noise source blended into the carrier of the bands above ~4 kHz, still gated by the modulator's envelopes — so consonants articulate even over a dull carrier, and only when the modulator actually has high-band energy (pinned: a silent carrier with an HF-rich modulator speaks at sibilance 1; a low-only modulator stays quiet). At the default 0 the original silent-carrier contract holds exactly, bit-identical — turning it up deliberately relaxes that contract for the top bands. The noise is deterministic per seed, family doctrine.

mix — the synth under its own robot voice

Equal-power blend of the dry carrier against the vocoded output — the classic parallel move (the pad fades in under itself talking). Endpoints are exact: 100 is bit-identical wet, 0 returns the carrier untouched.

Choosing the two signals (the actual craft)

  • The carrier must have energy where the modulator has bands. The eternal vocoder failure is a dull carrier: a mellow sine pad gives the high bands nothing to gate, and consonants vanish. The house answer is upstairs in this book — a tap.vco~ saw stack (harmonics forever, and the analog section keeps it moving) is a nearly ideal carrier; noise (tap.noise~) blended in restores the s and t sounds that even a saw can't carry.
  • The modulator wants articulation, not fidelity. Overdriven, compressed, even cheap-microphone speech vocodes better — what matters is envelope contrast between bands, not beauty.
  • Nobody said voice. Drums modulating a pad turns the pad into rhythm; a cello modulating noise is a ghost. The machine imposes any moving envelope on any body.

Recipes

  • The talking synth: speech → left; tap.vco~ saw stack + 10 % noise → right; response_interval 30, q middling, and enunciate like you're annoyed.
  • The choir: sustained "aah"s → left; detuned saws → right; response_interval 250. Consonants don't matter; vowels are the chord.
  • Rhythm transfer: a drum loop → left; anything sustained → right; short response_interval. The drums play the pad.

When it is not the right tool

  • Pitch correction or transposition — a channel vocoder never changes the carrier's pitch; it only shades its bands. Pitch is tap.shift~ / tap.pitchaccum~ territory.
  • High-fidelity cross-synthesis. Twenty-four bands is a voice, not a spectrograph; for surgical spectral morphing you want FFT-domain tools (tap.spectra~ is the start of that corridor).
  • Formant preservation while shifting — related, but a different machine: tap.harmony~, which multiplies the voice itself instead of wearing it over a carrier.

Checkpoint

Two matched 24-band banks, 50 Hz–12 kHz: the modulator's per-band envelopes gate the carrier's bands, and everything you hear is carrier. q trades crispness against smoothness, response_interval is the mouth's speed, and the craft is almost entirely in feeding it a bright, busy carrier and an articulate modulator. The machine is simple; the casting is everything.

A gate for every bin

A noise gate is a bouncer with one rule: too quiet, you don't get in. Useful, but blunt — when the signal plays, all the noise under it walks in too, and when the signal stops, the gate slams on room tone. tap.nr~ hires a thousand bouncers instead: it transforms each STFT frame and applies the threshold per frequency bin, so the quiet bins between your signal's partials close while the loud ones stay open. Hiss disappears from the gaps in the spectrum, not just the gaps in time. This chapter is the two knobs, the two costs, and the one artifact to listen for.

Companion material: the reference page and help patcher in the TapTools-Max package; the kernel's Catch suite pins the reconstruction claims below.

The contract: transparent until it isn't

The object runs its own STFT — Hann window, 4× overlap, COLA-normalized overlap-add — and the engineering contract is pinned by test: with the gate open, the output reconstructs the input exactly (below 10⁻⁶), delayed by one FFT frame. Whatever tap.nr~ does to your sound, it is doing it on purpose with threshold and slope; the machinery itself is transparent. Also pinned: a tone below threshold is strongly attenuated; a tone above passes untouched.

Diagram of the STFT scaffold both spectral objects share: input ring, analysis window, FFT, the pluggable spectral operation, IFFT, synthesis window, and COLA-normalized overlap-add

The pump this object runs on — tap.nr~ is this scaffold with a per-bin downward expander in the middle.

The knobs, one by one

threshold — where quiet begins

The per-bin level (linear amplitude) below which a bin is attenuated. The craft: set it between your noise floor and your signal's quietest partials. Play the noisy source silent for a moment, raise threshold until the noise just vanishes, then stop — every further dB starts eating signal.

slope — how hard the door closes

The soft knee. 0 passes everything (bypass by another name); low values fade bins gently as they approach the threshold; high values approach a hard per-bin gate. And here lives the genre's famous artifact: push slope hard with threshold high and bins near the boundary flicker open and shut frame by frame — musical noise, a watery, birds-in-the-pipes chirping. The cure is almost always a gentler slope and a lower threshold, accepting a little noise instead of a lot of artifact. Half the craft of spectral gating is knowing when to stop.

FFT size — resolution vs. smearing (and the latency)

The frame size trades three things at once:

  • Frequency resolution: bigger frames separate closely spaced partials from noise between them — better gating for dense, tonal material.
  • Time smearing: bigger frames blur transients; a gate decision spreads across the whole frame. Percussive material wants smaller frames.
  • Latency: exactly one FFT frame, by construction. 2048 samples at 48 kHz is 43 ms — fine on a mix bus, noticeable on a live input.

Recipes

  • Location dialog cleanup: moderate frame, threshold found by the silent-passage method above, slope as low as removes the hiss. Listen to the pauses — that's where both the win and the artifact live.
  • Synth-line de-hiss: tonal material with stable partials is the best case — bigger frames, and the gate closes every bin the notes don't own.
  • Creative abuse: absurd threshold with a hard slope isn't repair, it's an effect — the signal reduced to its loudest spectral bones. The artifact becomes the instrument.

When it is not the right tool

  • Noise under the signal, not beside it. A gate — even per-bin — only removes noise where the signal isn't. Broadband hiss sharing bins with a broadband source needs subtraction/statistical methods, a different machine.
  • Hum and buzz. A 50/60 Hz family is a few known frequencies; surgical notches (tap.filter~) beat a thousand bouncers who all have to guess.
  • Time-domain gating with musical envelope shaping — attack/hold/release on the whole signal is a classic gate's job, and it doesn't smear transients.

Checkpoint

An STFT expander: per-bin thresholds close the spectrum's quiet gaps, the machinery reconstructs bit-faithfully when open (pinned below 10⁻⁶), and the price is one frame of latency plus the musical-noise artifact that appears exactly when threshold and slope are pushed past honest. Find the floor, close the door gently, and stop while the pauses still sound like air instead of water.

The spectrum, re-plumbed

Every process so far in this book treats the spectrum with respect: filters shade it, gates prune it, vocoders dress it up. tap.spectra~ re-plumbs it. Each output bin k is filled from input bin round(k · remap) — the spectrum's contents redistributed by a rule with no acoustic justification whatsoever. It is the one object in this book whose purpose is to sound like nothing in nature, and it is honest about it: the reference page has called it an "ultra-non-linear effect" since 2002. This chapter is what the rule does, why the results are inharmonic almost everywhere, and how to drive an effect whose sweet spots are narrow and strange.

Companion material: the reference page and help patcher in the TapTools-Max package; the kernel's Catch suite pins the two anchor behaviors below.

The rule, and its two pinned anchors

Inside the object's own STFT (the same Hann/4×-overlap engine as tap.nr~), the lower half of the output spectrum is assembled by reading input bins at k · remap, and the upper half is mirrored to keep the spectrum Hermitian — so the output is always real, whatever violence the remap did. Two behaviors are pinned by test:

  • remap 1 is the identity: the output reconstructs the input exactly, delayed by one FFT frame. Transparent machinery, like its sibling.
  • remap 2 moves input bin 2k to output bin k — the spectrum compressed toward the bottom: content from twice the frequency lands at half.

Why almost everything comes out inharmonic

Pitch shifting scales frequencies continuously; this remaps bin indices, quantized to round(k · remap). A harmonic series at f, 2f, 3f… survives integer remaps in recognizable form — remap 2 folds a harmonic spectrum roughly an octave down — but at remap 1.37 the partials land on a grid nature never drew: some merge, some vanish, spacings go irrational-ish. The result reads as bells, metal, ghosts of the input. That in-between space is the instrument. Sweep remap slowly across 1.0 and you can hear the sound leave reality and come back.

Two practical corollaries:

  • remap just above or below 1 (0.9–1.1) is the subtle zone — a detuned, phasey shadow of the input, cheaper than it sounds.
  • remap well below 1 stretches the low spectrum upward across the output (each output bin reads a lower input bin), thinning the top; well above 1 compresses everything into the bass and discards the input's top octaves entirely. Loud, dark, and blunt — usually wants a fresh brightness source afterwards.

The knobs

There is really one, plus the frame:

remap — the rule

Continuous. Identity at 1; integer values are the quasi-musical landmarks; everything between is the inharmonic wilderness. Automate it slowly — the per-frame quantization means fast sweeps step audibly, which is either the problem or the point.

FFT size — the grain of the grid

Bigger frames put the bins closer together, so the remap grid is finer: less quantization grit, smoother inharmonicity, more latency (one frame, as always) and more transient smearing. Smaller frames make the remap chunkier and more overtly digital. Unlike tap.nr~, where the frame is a fidelity question, here it is a flavor question.

Recipes

  • Bell foundry: harmonic material (a tap.vco~ saw, a piano) at remap 1.3–1.6, into a long reverb. Instant inharmonic percussion.
  • The shadow voice: speech at remap 0.95, mixed subtly under the dry — a wrongness the ear notices before the mind does.
  • The corridor: automate remap 1.0 → 2.0 over a minute under a sustained chord — a slow departure from consonance that lands, at exactly 2, somewhere almost stable again.
  • Stacked plumbing: two in series at remap a and b is a remap at a·b with two layers of quantization grit — the grit is the reason to do it.

When it is not the right tool

  • Musical transposition. The remap is spectral plumbing, not pitch shifting: use tap.shift~ for clean intervals, tap.pitchaccum~ for the spiral.
  • Harmonizing or formant work. Nothing here knows what a formant is; the rule moves bins, not vowels.
  • Subtle timbre correction. Even at its gentlest this object is a character effect; EQ-shaped intentions belong with tap.filter~ or tap.svf~'s EQ modes.

Checkpoint

One rule — output bin k reads input bin round(k · remap), Hermitian-mirrored — inside a transparent STFT: identity at 1 (pinned), octave-fold at 2 (pinned), and an inharmonic wilderness everywhere between. The FFT size sets the grain of the grid, the sweet spots are narrow, and that is the appeal: this is the book's one unapologetic reality-distortion tool. Use it where nature's spectra have gotten boring.

The acid machine

The Roland TB-303 was designed to imitate a bass guitar, failed completely, and accidentally defined thirty years of dance music. What makes it unmistakable is not any one block — a saw into a lowpass is every synth ever made — but the coupling: accent drives the filter and the amplifier through shared circuitry with memory across notes, slide is a gate that refuses to let go, and the envelopes are fixed RC discharge curves with exactly one knob between them. tap.303~ is a circuit-informed model of that whole tangle; tap.diode~ is its filter as a standalone object; tap.303.seq~ is the other half of the instrument. This chapter is what each control trades, and what the measurements say the model actually delivers.

Companion material: the reference pages and help patchers in the TapTools-Max package, and two executed verification notebooks — tb303.ipynb for the voice and step_seq.ipynb for the sequencer — every number below is a measurement from one of them or from the kernel test suite. Provenance runs through Tim Stinchcombe's filter analysis, Robin Whittle's Devil Fish documentation, the x0xb0x schematics, and Robin Schmidt's Open303, whose measured calibrations several constants adopt verbatim.

What the hardware is, in one paragraph

One saw-core oscillator (the "square" is the saw through a transistor shaper, not a clean pulse), into a four-stage diode-ladder filter — not the Moog transistor ladder; the diode ladder's stages load each other, which is why its resonance is broader, less pure, and entirely its own — then a one-transistor amplifier. Two envelopes, both decay-only RC discharges: the Main Envelope sweeps the cutoff (the envmod knob decides how much), the VCA envelope is fixed. Accent makes the Main Envelope hotter and faster, routes it into the VCA, and charges a capacitor (C13) through the resonance pot — and because C13 doesn't fully discharge between closely spaced accents, runs of accented notes bloom, the famous wow. Slide holds the gate across the step boundary while the pitch CV glides through a ~60 ms RC. Everything about a note — pitch, gate, accent, slide — comes from the sequencer, not the panel. That is why this is three objects, not one.

The filter first: tap.diode~

The panel says "18 dB/oct"; the circuit is four poles whose asymptotic slope is 24 dB/oct with a shallower region near cutoff — Stinchcombe untangled this, and the kernel reproduces his published transfer function to 0.028 dB. Two behaviors are load-bearing and easy to get wrong:

  • The resonance feedback runs through a 150 Hz high-pass, so resonance thins as the cutoff drops — low notes squelch, they don't ring. Pinned by test: the ring-down Q falls with cutoff.
  • A stock 303 never quite self-oscillates, and neither does this filter at stock settings. That emerged from the modeled feedback high-pass rather than being programmed in, and it's documented as a trait, not a defect. (Push resonance past 1.0 — the bend range runs to 1.5 — and it will sing for you anyway.)

Like tap.ladder~ it has a solver choice: fast (default) or exact, which iterates the re-linearized solve to convergence on the true nonlinear loop. Measured across a matrix out to resonance 1.4 and +24 dB drive — beyond anything the hardware can reach — the two differ by at most −44.9 dBr, at 1.6–3.3× the CPU. The exact solver is there for the suspicious; the fast one is there for the patch. oversample (1/2/4, default 2) and a signal-rate cutoff in the right inlet round out the tap.ladder~ surface.

The voice: tap.303~, knob by knob

The attributes mirror the seven-knob panel; the calibrations are Open303's measured laws.

Signal-flow diagram of the 303 voice: pitch through slide into oscillator, shaper, coupling highpass, diode ladder, and VCA, with the accent bus fanning to the envelope, the C13 sweep capacitor, and the VCA, and the envelope-driven cutoff CV feeding the ladder

The blocks are ordinary; the red and amber wires are the 303. Accent touches three destinations at once, and C13 remembers across notes — the couplings are the instrument.

  • waveformsaw or square. The square is the hardware's shaped saw: −tanh(10^(36.9/20)·saw + 4.37), Open303's measured constants verbatim — rounded and notched, audibly not a 50 % pulse.
  • cutoff — the knob in Hz. Stock travel is the measured 302–2394 Hz; the attribute range (100–5000) is a flagged bend beyond the panel.
  • resonance — 0..1 is stock; up to 1.5 is the bend.
  • envmod — how much Main Envelope reaches the cutoff, with the hardware's measured law: 2/3 of the sweep goes above the knob position, 1/3 below, and the "gimmick" offset shifts the resting point down as you turn it up. The knobs feel right because the interaction is modeled, not just the ranges.
  • decay — Main Envelope decay, 200 ms–2 s. On an accented note the hardware ignores this knob and runs at ~200 ms; so does the model (adjustable via the accdecay bend, 50–2000 ms).
  • accent — how hard accented notes hit: louder and punchier (the envelope routing), and quackier (the C13 sweep, scaled by the resonance knob). The wow is measured: over a run of closely spaced accents the cutoff peak builds by ×1.94, and decays back within ×0.998 once the accents stop. Consecutive accents at high resonance are the entire genre.
  • tuning, gain — cents and dB. Plumbing.

The envelopes carry the schematic's fixed interrelations: MEG attack ~3 ms, VCA attack ~3 ms with a measured ~1.23 s decay chopped at gate-off, 50 ms when accented. None of these have knobs on the hardware, so none of them have knobs here — except through the documented Devil-Fish-style bends (slide 10–500 ms, attack 0.3–30 ms, accdecay, and drive ±24 dB into the ladder, where the diodes compress: +24 dB of gain buys only 9.2× of RMS). All stock at their defaults.

Phase 2 added vca clean|warm: the one-transistor class-A stage as a slope-normalized biased saturator, in the hardware's signal order. The distortion tracks the envelope — measured 5.4 % difference signal on quiet notes, 11.5 % on hot accents — so warm thickens exactly where the hardware does. clean (default) is bit-identical to phase 1.

House machinery throughout: seed/tolerance per-unit component spread (an mc. stack of 303s with different seeds detunes and drifts like a wall of real units), 16 preset-morph slots with factory acid in 1–8 (squelch, sub, screamer, rubber, knock, bloom, overdriven, glass), and per-sample ramps on every parameter.

The note interface, and why slide is free

tap.303~ is TapTools' first pitched instrument, and its inlets are the package-wide melodic contract: pitch as a MIDI note number signal in the left inlet, gate with amplitude-as-accent in the right — 1.0 is a plain note, 2.0 fully accented (depth = amplitude − 1). Slide needs no input at all: a pitch change while the gate is held is a slide — legato, no envelope retrigger, the ~60 ms RC glide — which is exactly the hardware's own definition. A note <pitch> [accent] [slide] message covers patching without signals.

The other half: tap.303.seq~

Half the 303's sound is sequencer behavior, so the sequencer emits the voice's contract verbatim: a pitch signal and a gate signal, clocked by a phase ramp (0..1 per pattern, a phasor~). Per step: pitch, gate/rest, accent, slide. The measured facts, from the sequencer notebook:

  • Steps land on the analytic grid within one sample; the gate opens at the step start and closes at 0.5 of the step (Open303's stepLength).
  • A slid step is approached with the gate held: 16 gated steps with 3 slide flags produce exactly 13 note-ons — the other three arrive legato, pitch stepping on the boundary sample, and the voice glides.
  • Accented steps gate at 2.0; transpose shifts live, like the hardware's transpose mode without the mode; swing and pattern slots with cycle-quantized recall are shared with the drum rows (next chapter).

The 1981 pitch-mode/time-mode data entry is deliberately not recreated. You keep the data model; you lose the part everyone hated.

When it is not the right tool

  • You want a generic bass synth. tap.vco~ + tap.svf~ + tap.adsr~ give you ADSRs, waveform variety, and a filter that behaves. This object's value is its refusal to decouple.
  • You want the filter without the biographytap.diode~ alone, or tap.ladder~ if you want the Moog character instead of the 303's.
  • You want polyphony. It's a monosynth; mc. gives you many monosynths, which is not the same thing as a polysynth and shouldn't be.

Checkpoint

A diode ladder that matches the published analysis to 0.028 dB and won't self-oscillate until you bend it; a voice whose envelopes, accent path, and C13 memory come from the schematic, with the wow measured at ×1.94 across an accent run; slide as pure gate-hold, so legato falls out of the note contract; and a sequencer that emits that contract sample-accurately. The coupling is the instrument — and every claim above has an executed notebook cell behind it.

The drum machine

The Roland TR-808 is the most thoroughly analyzed drum machine in the academic literature, and the reason is charming: the whole instrument is analog synthesis. No samples anywhere — every sound is a small circuit, and most of them are variations on about four ideas. The tap.808.* family recreates the eight voice channels circuit block by circuit block, one external per channel, and tap.808.seq~ supplies the machine's other half as one sequencer row per patch cord. This chapter is the family tour: the shared trigger contract, each voice's character and knobs, and the calibration pass against a real unit that the numbers come from.

Companion material: each voice's reference page and help patcher, the family overview patcher (tap.808.maxhelp — all eight voices sequenced off one phasor~), the tr808_calibration.ipynb notebook, and the step_seq.ipynb sequencer notebook. Provenance runs through the Werner–Abel–Smith papers (DAFx-14 and companions) and the TR-808 Service Notes, read component by component; every magic constant in the kernel headers carries its schematic designator.

One trigger to rule them all

On the hardware, every voice hangs off a common trigger bus: the CPU's 1 ms pulse rides a voltage between 4 and 14 V depending on the accent circuit, and a hotter pulse excites each circuit harder — more punch, slightly different timbre — not merely louder. The family keeps that literally: every voice fires on a signal rising edge, and the edge's amplitude (0..1) is the accent, mapped onto the 4–14 V bus. bang and trigger 0.7 messages cover the scheduler side. Filter states persist across triggers, so fast rolls interfere with the ringing tail like the hardware — no machine-gun effect. And because the excitation is a voltage, anything that makes an edge can play the kit: a click~, an envelope, a tap.303.seq~ gate, or the row object built for the job.

The voices

tap.808.kick~ — the bridged-T with a biography

The bass drum is a damped bridged-T resonator (~49.4 Hz from the modeled component values; Roland's chart optimistically says 56, real units measure as low as 48) with three behaviors that make it the kick, all emergent from the modeled schematic: for the first ~6 ms the envelope saturates Q43 and the resonator sits near ~129 Hz — the attack punch, which is a different mechanism from the famous downward pitch "sigh" (leakage through R161, the paper's fitted nonlinearity); and a retriggering pulse re-excites the center node as the envelope collapses so the note doesn't step down. Panel knobs: decay (seconds of ring at the top), tone (click at ~7 kHz down to ~300 Hz), level. Paper-documented bends, stock by default: tuning, pulse, sigh, attack — turn sigh 0 and the pitch relaxation disconnects, exactly as the bend does on the bench.

Calibration: against a real unit's knob-gridded sample set, the fundamental sat within 2.4 % at every tone/decay position and the −40 dB decay endpoints within 6 % (72 ms → 2.36 s measured, 69 ms → 2.42 s modeled) — no constant needed changing.

tap.808.snare~ and tap.808.clap~ — resonators plus noise

The snare is two bridged-Ts (the late-revision ~173/336 Hz pair) with a trigger divider and the "snappy" path — enveloped noise, band-limited around 4 kHz to the measured unit. Fundamentals calibrated within 1.2 %, including the mode flip at tone-max. The clap channel (@model clap|maracas) is the Service Notes' Figure-13 circuit: band-passed noise near 2 kHz through a VCA driven by a three-teeth sawtooth retrigger — the "multiple hands" transient — plus the Q70 reverberation tail. The maracas mode is the same noise voiced short and bright.

tap.808.hat~ and tap.808.cymbal~ — the metal bank

Six Schmitt-trigger square oscillators (205.3, 369.6, 304.4, 522.7 Hz plus the two trimmer-tuned at 800 and 540, duty 47.98 %) feed two bandpass voicings near 3.4 and 7.1 kHz. Werner et al. measured that resistor variance puts any given unit up to ~20 % off those frequencies — which is why no two 808s' cymbals sound alike, and why seed/tolerance exists: every seed is a different unit off the line, and an mc. stack of cymbals decorrelates like real hardware. The hats are one object with two trigger inlets because on hardware they are one circuit with two envelope paths and a choke — closed chokes open (the Q23/R173 path), pinned by test, and unimplementable as separate externals. Open-hat decay spans the chart's 90–600 ms; the cymbal's two separately enveloped bands cover its 350–1200 ms "sizzle" span. tap.808.cowbell~ taps just the 540/800 pair into the ~860 Hz voicing with a two-slope envelope; more cowbell is a patching decision.

tap.808.tom~ and tap.808.rim~ — the resonator variations

Six sounds on two objects, as the hardware switches them: @size low|mid|high × @model tom|conga. Congas are the tom circuit without its noise layer, tuned differently; the toms add the D80/D81 attack pitch fall and a pink noise layer. The rim channel is @model rimshot|claves: the rimshot's ~1667 + 455 Hz crack with the swing-VCA's harmonics, versus the claves' pure ~2500 Hz tick. Tunings sit within ~4 % of the measured unit.

The bridged-T resonator circuit: an op-amp with capacitive arms, a resistive bridge, and a leg to ground, triggered through an injection resistor, with the kick's per-sample leg modulation drawn in red — and the eight voices grouped by how they use it

Roland's universal voice circuit. Eight voices, one network — the kick earns its punch by modulating the leg per sample.

The calibration pass, honestly

The §7.2 calibration ran against a real TR-808 (s/n 103852) recorded from the individual outs with knob positions encoded in the filenames — a 0/2.5/5/7.5/10 dial grid, 116 samples — which upgraded "sounds right" to a quantitative per-knob-cell comparison. Identical measurements (spectral-peak fundamental, −40 dB decay, power centroid) ran on both sides. The pitches were already right nearly everywhere; what the pass actually changed was time: tom, conga, cowbell, and clap tails roughly doubled to match the unit, the snappy was band-limited and re-enveloped, the rimshot re-voiced low-dominant, the cymbal's decay span corrected. Each kernel header carries its residuals. The lesson generalizes: schematics get you the frequencies; recordings get you the envelopes.

The other half: tap.808.seq~

One row of the 16-step sequencer, as an object: feed it a phase ramp (0..1 per pattern, a phasor~) and it emits trigger impulses whose amplitude is the step's accent — the family contract, straight into any voice. Twelve rows off one phasor are the hardware's panel, sample-locked forever; the accent row falls out of giving every row the same accents list. The measured facts, from the sequencer notebook: steps land on the analytic grid within one sample; the pinned levels are plain 0.01 (the 4 V base — an un-accented hit still strikes the circuit) and accented 0.5 (the accent knob at noon; 1.0 is the full 14 V); swing delays the off-16ths by exactly swing/2 of a step; a length 12 row against 16s is the triplet pre-scale generalized to polymeter; and pattern slots with cycle-quantized recall are the A/B-half and fill switching as one message. pulse widens the impulse into a held gate when you'd rather drive tap.adsr~ than a drum.

When it is not the right tool

  • You want a kick, not the kick. A sine with an envelope is cheaper and takes EQ more politely. This family's value is the circuit behavior — the attack jump, the choke, the accent-as-voltage.
  • You want your own drum sounds. These circuits are what they are; seed, the documented bends, and the panel knobs bend them, but a sampler is a sampler.
  • You want 909 hats. The 909's metal is sampled; this machine's is six square waves. Different instrument, different chapter, maybe someday.

Checkpoint

Eight channels, four circuit ideas — bridged-T resonators, a shared metal bank, noise paths, swing-VCAs — under one amplitude-as-accent trigger bus, calibrated per knob cell against a real unit and honest about what changed (the tails) and what didn't (the tunings). The hats choke because they share a circuit; the cymbals decorrelate because resistors do; and the sequencer row emits the same voltage idea the voices drink, so the whole kit runs off one phasor ramp. The machine's two halves, both measured.

The note you meant

Every sung note is two notes: the one that happened and the one you meant. tap.tune~ measures the distance between them and closes it — how fast it closes it is the whole instrument. Closed slowly, nobody knows it was there. Closed instantly, everybody knows: that snap is the most famous vocal effect of the last twenty-five years. One object, one time constant, both worlds.

A short history matters here, told plainly. The classic pipeline — detect the pitch, snap it to the nearest allowed note, retune by time-domain resynthesis — was patented in 1998 and the patent expired in 2018, which is why a whole field of tuners exists today and why this object can implement the technique from the literature. The famous product name remains a live trademark, which is why this object is called tap.tune~ and this chapter says "hard snap" instead. And editing individual notes inside a chord remains patent-fenced territory — tap.tune~ is monophonic by design, not by omission. (None of this paragraph is legal advice; the project's own ship-gate is a freedom-to-operate review.)

Companion material: the reference page and help patcher in the TapTools-Max package, the runtime maxtest, and two executed notebooks — tune.ipynb here and pitchshift.ipynb in the DspTap repo — that measured every claim below.

Signal-flow diagram of tap.tune~: a per-hop brain of YIN, target mapper, glide, and ratio over a per-sample path through the input ring into the selectable resynthesis backend

A per-hop brain over a per-sample corrector, with one seam where three resynthesis engines interchange.

The knob that is the instrument: speed

speed is the time constant, in milliseconds, of the glide onto the target note.

  • 0 ms — the hard snap. The correction lands within a detection hop (~5 ms). Vibrato gets quantized into terraces; note transitions become instant staircase steps. This is the effect, worn on the outside.
  • 10–40 ms — classic correction. Fast enough that a listener hears "a singer with good intonation," slow enough that the attack of each note — where identity lives — is not robotic. The default is 20.
  • 100 ms and up — intonation leaning. The corrector arrives so late it only tames drift; vibrato passes through nearly untouched.

The notebook's pitch-track figure shows all three glides onto the same 46-cent-sharp note; the kernel test pins the exponential's arrival. There is also amount (0–100%): a fader on the correction distance itself. 100 lands on the target; 50 splits the difference — a gentler kind of honesty that keeps a performance's shape while shrinking its errors.

Telling it what is allowed

The corrector never invents a target; it snaps to the nearest note you allowed.

  • key + scale — the usual contract: @key d @scale major and every detected pitch pulls toward the nearest D-major degree. Presets: chromatic, major, minor, harmonic, melodic, pentatonic, minorpentatonic.
  • notes — the twelve toggles, absolute pitch classes C through B, panel-style: notes 1 0 0 0 1 0 0 1 0 0 0 0 snaps everything to a C-major triad, which is less a correction than an arrangement decision.
  • mode midi — the target is the nearest currently held MIDI note (note 64 100 holds E4; velocity 0 releases; flush clears). Hold one note and everything becomes that note; hold a changing chord's roots and the corrector is suddenly a performable melody-mangler. No notes held means no correction — the object never guesses.

An empty mask behaves the same way: nothing allowed, nothing changed.

Three engines, one corrector: backend

Detection, targeting, and the glide are shared; only the resynthesis swaps. All three land the same intonation — the notebook drives the same vibrato "voice" through each and all three settle on 220.00 Hz — so the choice is about character and latency, not accuracy.

backendwhat it ischoose it forlatency @ 48 kHz
graintwo-tap delay-line, window locked to the detected period (the tap.shift~ engine)the default; lowest latency, waveform-preserving, happy on any materiala few ms
psolatrue TD-PSOLAvoice — it preserves formants by construction~36 ms
pvocpeak-locked phase vocoderdense, harmonically rich material; pairs with formant~21 ms

Switching live is click-safe: the incoming engine starts from silence and fades in rather than splicing stale audio. One honest caveat per engine: grain colors sustained unpitched input with a mild moving comb (the known trade of its class); psola wants harmonic material — on a pure sine shifted far, its output legitimately thins (the machine chapter explains why that is the same property as its formant preservation); pvoc smears sharp transients slightly, as every phase vocoder does.

Keeping the singer's mouth: formant

A correction of thirty cents moves formants thirty cents — nobody hears it. A MIDI-mode command of five semitones moves them five semitones — everybody hears it; that is the chipmunk. @formant 1 enables LPC formant preservation on the pvoc backend: the pitch moves, the vocal tract's envelope stays where the singer put it. The notebook corrects a synthetic voice up 5.5 semitones both ways; with the flag on, the formant bump stays put (band-energy ratio 730:1 in its favor). psola needs no flag — formant preservation is its resampling rule — and grain ignores the flag.

Letting it find the key: autokey

@autokey 1 starts a learner: every voiced detection drops its pitch class into a histogram that forgets with about a minute of memory, scored against the published Krumhansl–Kessler key profiles. Two design decisions worth knowing:

  • It never acts on its own. A key estimate that silently re-aimed your targets mid-phrase would be a bug wearing a feature's clothes. getkey asks (the right outlet answers key d major 0.95, or key none in the first half-second); applykey adopts the estimate into the key and scale attributes — visibly, where you can see and undo it.
  • It forgets on purpose. The one-minute memory means a modulation stops arguing with the old verse about as fast as you stop playing it.

The kernel test plays a D-major scale and reads back D major at 0.95 confidence; an A harmonic-minor melody reads as A minor.

The right outlet

While the input is voiced, the right outlet reports pitch <midi> <hz> every @interval milliseconds (default 50; 0 disables; a pitch -1 0 marks the end of voicing). That is a free tuner display, a melody recorder, or the control signal for whatever you want to drive with the singer's pitch — and it is the same detector the corrector itself uses, so what you see is what it acted on.

Recipes

  • Invisible repair: @scale major @key (your key) @speed 25 @amount 80. The 80 keeps a little humanity in the intonation; nobody will name what changed.
  • The famous one: @speed 0 @scale minorpentatonic. Fewer allowed notes make the terraces wider and the snap prouder. Add melisma.
  • One-note choir: @mode midi @speed 5, hold a note, feed it speech. Everything becomes chant on that pitch.
  • Formant-true transposer: @mode midi @backend pvoc @formant 1 @speed 10, play a melody against a held vocal — a harmonizer that keeps the singer's identity.
  • Tuner display only: @amount 0 @interval 20 — the object corrects nothing and the right outlet becomes a clean pitch stream.

When it is not the right tool

  • Chords. The detector is monophonic; a chord reads as garbage or as its loudest note, and per-note polyphonic editing is deliberately out of scope (see the history paragraph). Split voices first, or don't.
  • Drums, breath, speech consonants. Unpitched input passes through with no correction — by design — but the grain engine adds its mild comb coloration to sustained noise. For processing unpitched material there are better rooms in this house.
  • Creative shifting. If the goal is an interval rather than intonation, tap.shift~ is the plain shifter, tap.harmony~ the formant-preserving chord stack, and tap.pitchaccum~ the spiral; tap.tune~ always measures first and that measurement is latency you don't need.

Checkpoint

Detect, snap to the nearest allowed note, glide at speed — that is the whole machine, and speed is the dial between honesty and effect. Targets come from key + scale, twelve toggles, or held MIDI notes; three resynthesis engines trade character against latency while landing the same intonation; formant keeps the singer's mouth in place when corrections get big; autokey learns the key but only ever suggests. The right outlet tells you what it heard. And when the input isn't a single pitched voice, the honest move — which the object makes — is to change nothing.

Distortion with a memory

Every distortion plugin can bend a transfer curve. tap.overdrive~ is built on the observation that the pedals people actually love — the Tube Screamer lineage, and specifically the Mad Professor Little Green Wonder that served as this object's listening reference — don't apply one curve to the whole spectrum. Their clipper lives inside an op-amp's feedback loop with frequency-dependent parts around it, and that loop is most of the sound: bass sees less gain and stays tight, mids break up first, and the knee never quite flattens because the clean signal always rides through. A memoryless waveshaper — including both modes of the Jamoma-era tap.overdrive~ this object succeeds — structurally cannot do any of that. This one can, because the shaper sits inside a lowpass feedback loop: distortion with a memory.

Companion material: the reference page and help patcher in the TapTools-Max package, and the verification notebook, where every number below is an executed, plotted measurement of the shipping kernel. The figures in this chapter are measurements too — regenerated from the same kernel through the C ABI by book/figures/overdrive.py, never drawn by hand.

What the loop buys

The claim worth leading with, because no static curve can make it: the object's small-signal gain tilts with frequency, and the tilt grows with drive. Measured between 80 Hz and 4 kHz, the tilt is +5 dB at drive 0 (just the voicing EQ), +16.3 dB at drive 0.5, +17.2 dB at drive 0.9. Low frequencies are pinned near-clean by the feedback while mids and highs take the full drive gain — so a low E stays articulate under the same setting that saturates the pick attack. That is the Tube Screamer "tightness" in one plot:

Small-signal gain versus frequency at drive 0, 0.5, and 0.9: the curves tilt progressively steeper as drive rises, with bass pinned and mids lifting

The measured headline. A memoryless shaper's version of this figure is three horizontal lines.

The second structural trait: the transfer never flattens. A unity clean path is summed around the clipper — the non-inverting op-amp topology — so however hard the shaped part saturates, output keeps rising with input (measured strictly monotonic at every drive setting). The old sine-shaper mode's hard ±1 plateau, a large part of what read as "digital," is gone by construction.

Output peak versus input peak at three drive settings: every curve keeps rising with reduced slope, none goes flat

Compression without a ceiling: the slope falls as drive rises, but never to zero.

The knobs, one by one

drive — 0 to 1, edge-of-breakup to saturated

Normalized, like every musical parameter on this object, with the perceptual mapping done inside (the knob sweeps the clipper's gain from +6 to +46 dB, with a level compensation tracking it). drive 0 is a pedal's gain knob at full counterclockwise — still warm, not bit-clean; bypass is the clean switch. The normalized range maps directly onto MIDI/OSC controllers, and onto Q15/Q31 fixed-point for the embedded ports this kernel is written to survive.

body — the signature voicing control

The LGW's defining knob, reproduced as linear pre/post EQ around the clipper (that's what it is in the pedal — voicing, not nonlinearity). Toward −1, fuller lows reach the clipper and the top gets a slight shelf lift; toward +1, the lows thin and tighten and an upper-mid bell pushes forward — centered at 1150 Hz, deliberately above the classic TS hump. Measured at the extremes: 100 Hz moves by 10 dB, the 1150 Hz push adds 4 dB, the counterclockwise treble lift is +2.5 dB at 8 kHz. The exact centers and gains are by-ear placeholders pending the in-Max voicing pass against LGW demos — the shape of the control is final, the seasoning isn't.

Small-signal response at body −1, 0, and +1: fuller lows and a top lift counterclockwise, thinner lows and an upper-mid push clockwise

The knob's whole range. Note the crossover around 500 Hz: body trades lows against upper mids around a stable center, like the pedal.

asymmetry — the even harmonics the old object couldn't make

Both Jamoma modes were odd functions: odd harmonics only, the entire "warmth" vocabulary absent. asymmetry biases the clipper: at 0 the path is exactly symmetric (measured H2 at −151 dB — the numerical floor), and raising it brings the even series up smoothly (H2 at −26 dB by asymmetry 0.6). The default sits at 0.15, a small nonzero warmth chosen by ear. Asymmetric clipping generates DC, so a DC blocker sits permanently after the clipper — measured output mean under full drive, full asymmetry: 10⁻¹⁰. (The original TTOverdrive contained a DC blocker whose output was computed and then discarded; this one is load-bearing.)

Harmonic spectra of a 220.5 Hz tone at asymmetry 0 and 0.6: the left panel shows odd harmonics only, the right adds the full even series

The same tone, the same drive — the only change is asymmetry, and the even series (H2, H4, …) appears between the odd lines.

oversample — 1, 2, 4, or 8; default 4

Clipping makes harmonics; harmonics past Nyquist fold back as inharmonic junk. At 1× a hard-driven 5 kHz tone puts its folded seventh harmonic at −22 dB relative to the fundamental — clearly audible garbage at 12993 Hz. At the default 4× the same component measures −36 dB, with the true harmonics unchanged. Turn it down to 1× only when CPU matters more than the top octave, or when you want the fizz.

preamp, output, smooth, bypass, mute

Input and makeup gain in dB (±24) — the only unit-bearing parameters, because gains are the one place real units belong. Everything ramps click-free over smooth milliseconds (default 20).

Where it sits in a patch

Mono by design; wrap it in mc. for multichannel like the rest of the package. It takes line-level signals as happily as guitar DI — the drive mapping is normalized to full-scale digital, not to pickup output. For the LGW move, start at drive 0.4, body -0.3, asymmetry 0.15 and ride body against the source's low end. For a clean boost that just thickens, drive 0 with asymmetry 0.3. For fuzz territory this is the wrong object on purpose — the loop keeps pulling it back toward articulation.

Every claim above is pinned twice: as an executed measurement in the notebook, and as a hard assertion in the kernel's Catch2 suite (tests/overdrive_test.cpp), which CI runs on every push. The math behind the loop — including why it had to be solved zero-delay, and what happens if you don't — is in the machine chapter: The clipper in the loop.

Solving the filter on paper: svf.h

The user-facing chapter promised that tap.svf~ is "unconditionally stable under per-sample cutoff modulation" and that its morph corners are "bit-identical to the discrete modes." Promises like that are either mathematical facts or marketing. This appendix does the math: it derives the filter the way the file was actually designed, then walks the engineering decisions that don't show up in a Bode plot — and why each one beat its alternative.

The reference is Andy Simper's Cytomic technical papers ("Solving the continuous SVF equations using trapezoidal integration and equivalent currents"), specifically the SvfLinearTrapOptimised2 form. What follows is the same derivation with the file's variable names.

The analog prototype, and where digital versions go wrong

The state-variable filter is two integrators in a loop. With cutoff ω and damping k = 1/Q, the continuous equations are:

v1' = ω · (v0 − k·v1 − v2)     (band state: input minus damping minus low state)
v2' = ω · v1                   (low state: integral of band)

Lowpass is v2, bandpass v1, highpass v0 − k·v1 − v2 — every response lives in the same two states, which is what makes an output mix (and therefore a morph) possible at all.

The textbook digital version (Chamberlin) discretizes with explicit Euler: each integrator uses the previous sample's value. That inserts a unit delay into the loop, and a delay in a feedback loop is a stability bomb with a frequency fuse: the design blows up as fc approaches fs/6, and modulating the cutoff re-lights the fuse every sample. The classic workarounds (oversample it, clamp it) treat symptoms.

Trapezoidal integration, and the algebraic loop

The fix is to integrate with the trapezoidal rule — average the old and new derivative — which in filter terms is the bilinear transform. Define the prewarped gain the file computes once per cutoff change:

g = tan(π · fc / fs)

(The tan is the prewarp: it makes the digital filter's response at fc exactly match the analog prototype's, all the way to Nyquist. This single line is why self-oscillation later measures 999.7 Hz for a 1 kHz setting rather than drifting flat.)

Trapezoidal integration of state s with input x is s_new = s + g·(x_old + x_new). Grouping the "old" terms into a memory variable — Simper's equivalent current ic = s + g·x_old — each integrator becomes:

s_new = ic + g · x_new         with ic updated as ic_new = 2·s_new − ic

But notice the trap: x_new for the first integrator is the new band value, which depends on the new low value, which depends on the new band value. The new sample appears on both sides — an algebraic loop, exactly the "zero-delay feedback" the initials ZDF refer to. Instead of breaking the loop with a delay (Chamberlin's sin), we solve it. It is linear, so substitution gives a closed form. With v0 the input and ic1, ic2 the two equivalent currents:

v1 = a1 · ic1 + a2 · (v0 − ic2)          the band state, solved
v2 = ic2 + g · v1                        the low state, then follows
where  a1 = 1 / (1 + g·(g + k)),  a2 = g · a1

Those are precisely the file's per-section solve constants (a1, a2, a3 = g·a2 caches the product used by the low state), and the state update is the canonical TPT pair ic1 = 2·v1 − ic1, ic2 = 2·v2 − ic2.

Why this is unconditionally stable, even modulated: the trapezoidal rule is A-stable — it maps the entire left half of the s-plane (every stable analog filter) inside the unit circle, for any g > 0. Change g every sample and each sample still computes a passive, energy-consistent step; there is no regime of fc or modulation rate where the update gains exceed unity. The notebook's 90 Hz-LFO-through-five-octaves torture test isn't surviving by margin; it's surviving by theorem.

The TPT SVF core as a diagram: two trapezoidal integrators, the damping and low feedback into the input sum, and the downstream output mixer

The loop the algebra just solved, and the mixer the next section explains.

The output mix, and why morph corners cost nothing

Every response is a weighted sum over the same solved values:

y = m0·v0 + m1·v1 + m2·v2

lowpass   m = (0,  0, 1)        notch     m = (1, −k, 0)
bandpass  m = (0,  1, 0)        peak      m = (1, −k, −2)
highpass  m = (1, −k, −1)       allpass   m = (1, −2k, 0)

mode_morph linearly interpolates the mix vector around the circle LP → BP → HP → notch → LP. Two facts follow by construction, not by tuning:

  • At a corner, the interpolated vector equals the discrete mode's vector exactly — same floats, same states, same arithmetic. The notebook's measured max difference of 0 is not a tight tolerance; it is an identity.
  • Morphing is free. The states don't know the mix exists; sweeping it can never destabilize anything, because it is three multiplies downstream of the filter.

The parametric-EQ trio (bell, shelves) is the same machinery with mix weights that depend on a gain factor A = 10^(dB/40), straight from Simper's tables — and it always runs a single section, because cascading an EQ stage squares its boost: two +12 dB bells are a +24 dB bell, which is never what the user typed.

The cascade: Butterworth spread, resonance on the last section

Orders 4 and 8 run two and four sections at the same cutoff. Stacking identical Q = 0.707 sections would droop the passband (each contributes its −3 dB early); instead the sections take the Butterworth Q spread — the Qs whose product of section responses is maximally flat:

Q_i = 1 / (2·cos θ_i),  θ_i the Butterworth pole angles
order 4: 0.5412, 1.3066        order 8: 0.5098, 0.6013, 0.9000, 2.5629

That is why the measured response sits at −3.01 dB at fc at every order. User resonance then sharpens only the final (highest-Q) section, via

Q_res = Q_base / (1 − r),  r ∈ [0, 1)   (clamped at 1 − 10⁻⁴)

— one clean resonant peak riding a flat passband, rather than four peaks compounding. The inverse mapping (resonance_from_q) exists so the wrapper's q message round-trips exactly.

The driven circuit: one saturation, one pass

The driven circuit places tanh on the band node — in the damping path, where an OTA's transconductance actually compresses. That placement is the whole design: as amplitude grows, the effective damping k·tanh(v1)/v1 grows with it, which is an automatic gain control wrapped around the resonance. Push the loop gain slightly past the oscillation threshold at resonance 1.0 and the filter must oscillate (the linear model's poles are outside the circle) but cannot run away (the saturation restores effective damping as amplitude rises). Bounded self-oscillation is not a limiter bolted on; it is the fixed point of that tug-of-war. An all-zero state solves the equations too — hence "give it a ping."

Solving a nonlinear zero-delay loop exactly needs iteration. The file uses the one-pass scheme shared with tap.ladder~'s solver_fast: solve the linear ZDF prediction for the band node, saturate it, commit. The error of that shortcut is second-order in how much tanh bends over one oversampled step — and the driven circuit always runs oversampled (2× default), with 4th-order Butterworth anti-image/anti-alias biquad pairs on the way up and down. At these rates the one-pass and iterated answers are audibly identical; the ladder file, which drives its nonlinearity much harder, is the one that also ships a Newton option.

The engineering ledger

Decisions visible only in the code, with their reasons:

  • Two-tier coefficient update. Recomputing everything per sample costs a tan() plus the mix logic even when nothing changed. The file splits state: a shape tier (damping, mix weights, EQ gains — dirtied only when a non-frequency parameter or mode changes) and a cutoff tier (tan and the three solve constants — recomputed only when the incoming cutoff differs from the cached one). Signal-rate modulation pays for exactly what it moves. The benchmark ratchet recorded the win: modulated 2nd-order lowpass 36 → 19 ns/sample, modulated morph 77 → 28, bit-identical output (the morph-corner identity tests pin that "bit-identical" is literal).
  • ramp_to doesn't dirty the shape tier for frequency — the cutoff cache catches it. One branch, measurable at audio rates.
  • Multichannel by frame protocol. Coefficients are computed once per tick() and shared by every channel's process(ch, x) — an N-channel engine outside Max for the cost of one solve. The Max wrapper stays mono by house rule (mc. wraps it).
  • Allocation discipline. The only allocation is the per-channel state vector in prepare(); setters are wait-free and safe from the message thread while audio runs, because a "set" is a ramp target plus a dirty flag.
  • Anti-denormal guard on the states (the tap.comb~ idiom): a filter ringing out into silence otherwise wanders into denormal territory and multiplies its own CPU cost right when the music is quietest.
  • What is deliberately absent: fast-tanh approximations and a polyphase halfband resampler are both flagged in the file as candidates — and parked, because each changes output microscopically and the project's rule is that optimizations land only bit-identical or explicitly signed off.

Checkpoint

Trapezoidal integration turns the SVF's two integrators into a solvable linear system per sample — A-stability is where the modulation-proofness comes from, prewarping is where the tuning accuracy comes from, and the output mix is where morphing comes from, corner-exact by construction. Butterworth spread keeps cascades flat; resonance sharpens one section; the driven circuit's tanh placement makes bounded self-oscillation a fixed point rather than a feature. The rest is bookkeeping — and the bookkeeping was benchmarked.

The nonlinear loop: ladder.h

The user-facing chapter promised self-oscillation in tune (8009 Hz measured for an 8 kHz cutoff), THD that walks from 0.5 % to 33 %, comp recovering exactly the passband resonance eats, and a measured 13.5 dB from oversampling. This appendix derives all of it. The SVF appendix built the trapezoidal machinery; here it is wrapped in four tanh saturators and a feedback loop supposed to go unstable — and the engineering is solving a loop that no longer solves on paper.

The stage: one pole, trapezoidal, prewarped

Each stage is the analog one-pole lowpass y' = ω·(x − y), discretized with the trapezoidal rule exactly as in the SVF. Once per cutoff change, update_derived computes

g   = tan(π · fc / fs_os)      the prewarped integrator gain
m_g = g / (1 + g)              the solved per-stage gain, G below

and the per-stage step (tpt) is the standard zero-delay one-pole:

v = (x − s) · G        y = v + s        s ← y + v   (= 2y − s)

For a lone lowpass the tan prewarp is a nicety. Here it is the tuning system: the filter's oscillation frequency is set by where the stages put their phase, so pole mis-placement becomes pitch error — the failure of the classic Stilson/Smith ladder in the top octaves.

The loop: why the magic number is four

Four stages in series, global negative feedback: the stage-1 input is L − m_k·y4 (L the driven input, m_k = 4.0 * resonance). The 4 is the linear loop's oscillation threshold. At the cutoff a one-pole has response 1/(1 + j): magnitude 1/√2, phase −45°. Four in series:

|H⁴(fc)| = (1/√2)⁴ = 1/4        ∠H⁴(fc) = 4 · (−45°) = −180°

The input subtraction supplies the other 180°, so at fc — and only there — the loop phase is 360°. Barkhausen: oscillation begins at unity loop gain,

k · 1/4 = 1        ⇒        k = 4

So resonance = 1.0 (k = 4) is the mathematical edge, oscillation happens at the tuned cutoff, and self-oscillation frequency is the tuning test: the notebook measures 1000.2 Hz for a 1 kHz cutoff (0.02 % error) and 8009.0 Hz for 8 kHz (0.11 %) — the prewarp holding at the top of the keyboard, as promised.

The file allows k_res_max = 1.1, i.e. k = 4.4, "comfortably past self-oscillation." Past the edge the linear model diverges — but as amplitude grows, tanh's small-signal gain falls, the effective loop gain sags back toward 4, and the oscillation parks where they balance: the SVF driven circuit's fixed-point argument, no clipper needed. The notebook's oscillation (resonance 1.08) peaks near |y| = 0.10; the kernel test holds five seconds at k = 4.4 finite, under 2.0 peak, RMS steady within a 0.7–1.4× band. An all-zero state also solves the equations — hence the header's advice to ping it.

The ladder as a diagram: four tanh one-pole stages wrapped by the k = 4·resonance feedback, with comp and the pole-mix taps

The file as a schematic: the Barkhausen condition lives at the red tap.

The algebraic loop, solved linearly first

Zero-delay feedback through four stages means y4 depends on the stage-1 input, which depends on y4. Linearly, substitution closes it: chain the linear stage form y = G·x + B·s (B = 1 − G, s the held state) through all four with u = L − k·y4,

y1 = G·u + B·s1
y2 = G²·u + G·B·s1 + B·s2
y3 = G³·u + G²·B·s1 + G·B·s2 + B·s3
y4 = G⁴·u + G³·B·s1 + G²·B·s2 + G·B·s3 + B·s4

then name the state-only part S and solve:

S  = G³·B·s1 + G²·B·s2 + G·B·s3 + B·s4
y4 = G⁴·(L − k·y4) + S        ⇒        y4 = (G⁴·L + S) / (1 + k·G⁴)

That last expression is predict_linear verbatim — the code's (G2*G2*L + S) / (1.0 + m_k*G2*G2) with G2 = G*G and the same four-term S over m_s1..m_s4. For the linear ladder it is exact — the four-stage analog of the SVF's a1/a2 solve.

The saturators, and the one-pass commit

With tanh in every stage, the honest loop equation y4 = F(L − k·y4) has no closed form. The file ships two answers.

solver_fast (default) is Huovilainen-flavored prediction-correction: compute predict_linear(L) as if the saturators weren't there, then run the saturating stages once with that feedback value and commit (core):

t0 = sat(L − m_k·y4_est)
y1 = tpt(m_s1, t0, G),   y2 = tpt(m_s2, sat(y1), G),   ... y4 likewise

The committed y4 is not the y4_est the feedback used — that mismatch is the method's error. When the signal is small, tanh is the identity and the prediction is exact: the linear filter is recovered in the limit. The error grows only with how far tanh bends over one sample's state change — so drive × resonance is the failure axis, and oversampling (2× default) doubles as accuracy: it shrinks the per-step change being predicted.

solver_exact solves the true loop: Newton iteration on F(g) = y4_trial(L, g) − g, where y4_trial evaluates the four saturating stages for a guessed feedback value without touching state. Seeded by the linear prediction, clamped to ±3 (a tanh-bounded loop cannot park a fixed point far outside ±1), with a numerical derivative that falls back to the seed if it degenerates, at most 12 iterations to a 1e-12 residual. The commit reuses the same core path — with the converged g it reproduces the trial values while tpt advances the states. One code path, two accuracies.

How different are they? The kernel test renders both at drive 3 dB, resonance 0.5 and pins the maximum sample difference below 0.01; the stress test (drive 24 dB, resonance 1.1, asym 1.0) asks only that solver_exact stay finite and bounded. Audibly identical until drive and resonance are pushed — and solver_fast costs one saturated pass where Newton can cost dozens of trial evaluations per sub-sample.

asym: moving the operating point

Real ladder transistors don't match, so real stages don't saturate symmetrically. The model is an operating-point shift in every stage:

m_sat_bias = 0.3 · asym
sat(v) = tanh(v + m_sat_bias) − m_sat_dc      m_sat_dc = tanh(m_sat_bias)

The subtraction keeps sat(0) = 0 exactly — silence in, silence out. Expand tanh about the bias: a curvature term −tanh(b)·sech²(b)·v² appears only when b ≠ 0, and a v² term generates second harmonic and DC. The notebook measures the driven 2nd harmonic at −155.8 dB relative to the fundamental at asym 0 (numerical noise — tanh is odd), rising to −18.6 dB at asym 0.6. The DC is the rectifying side of the same v² term; the header owns it honestly and delegates to tap.dcblock~. Drive's own numbers: THD 0.54 / 3.45 / 16.50 / 33.07 % at 0 / 8 / 16 / 24 dB (notebook) — odd harmonics only, until asym says so.

comp: the passband bargain, quantified

At DC every stage passes unity; the closed linear loop gives

y4(DC) = L − k·y4(DC)        ⇒        y4(DC) = L / (1 + k)

Resonance eats the passband by exactly 1/(1+k). At resonance 0.9, k = 3.6: predicted 20·log10(1/4.6) = −13.3 dB; the notebook measures −13.2 dB. The compensation is a pre-gain (update_derived):

m_in_gain = 10^(drive/20) · (1 + comp·m_k)

At comp = 1 the input is multiplied by (1 + k) and DC gain returns to exactly unity — measured +0.0 dB — with a linear blend below. One honest note: the compensation multiplies the input before the saturators, so high comp at high resonance also leans harder on the tanh stages — like turning up the level into hardware; authentic, not a linear post-trim.

Pole mixing: the Xpander table

core returns a fixed weighted sum over the taps [t0, y1, y2, y3, y4] (k_c_mix). In the linear small-signal limit each tap is a power of the one-pole response H applied to u, so the mixes are polynomial algebra:

lp12:  y2                        =  H²·u
hp12:  t0 − 2·y1 + y2            =  (1 − H)²·u        weights {1,−2,1,0,0}
hp24:  (1 − H)⁴                  →  {1,−4,6,−4,1}      alternating binomial
bp12:  2·(y1 − y2) = 2·H(1−H)·u  →  {0,2,−2,0,0}
bp24:  4·H²(1−H)²                →  {0,0,4,−8,4}

The binomial rows are literally (1−H)ⁿ expanded. The bandpass factors are unity-gain normalizers: at fc, |H| = |1−H| = 1/√2 with phases ∓45°, so H(1−H) has magnitude 1/2 and phase 0 — the 2 (and 4 for its square) restore 0 dB at center. Measured high-side slopes: 23.4 dB/oct for lp24 (want 24), 11.7 for lp12 (want 12). Two caveats, both inherited from the analog original: under saturation the taps carry distortion products and the algebra is approximate (the header says so), and the feedback is always the full four-pole loop — a "12 dB" mode is a two-pole slope riding four-pole resonance, exactly as in an Xpander.

Oversampling: paying for tanh honestly

tanh generates harmonics without limit; above Nyquist they fold back inharmonically. run is the classic chain (the tap.verb~ pattern, self-contained per house rule): zero-stuff by the factor, scaling the retained sample by m_os to preserve passband gain; 4th-order Butterworth anti-imaging at 0.45 of the original Nyquist (fc_norm = 0.45/m_os); the nonlinear core at the high rate; a matching anti-alias Butterworth; decimation by keeping the last filtered sub-sample. Each Butterworth is two RBJ biquads at the textbook Q pair 0.54119610 / 1.30656296 = 1/(2·cos(π/8)), 1/(2·cos(3π/8)). Measured on a hard-driven 5 kHz tone: non-harmonic (alias) energy −30.1 dB at 1×, −43.7 dB at 4× — the promised 13.5 dB. The clamp fc ≤ 0.49·fs_os keeps 20 kHz legal at every factor.

The engineering ledger

  • One derived tier, not two. Unlike the SVF's split shape/cutoff caches, update_derived recomputes everything whenever anything moves (m_derived_dirty stays set while m_ramps_active > 0). Nearly every derived value reads several parameters (m_in_gain: drive, comp, and resonance) — a finer split buys little.
  • The signal-rate cutoff path re-dirties deliberately. process(x, cutoff_hz) recomputes for the override, then sets m_derived_dirty = true — "the cached G belongs to the override, not the parameter" — so the message-rate path never serves a stale one.
  • Ramps everywhere, counted. Every parameter rides a per-sample linear ramp (20 ms default); m_ramps_active makes idle one integer test. The kernel test bounds the worst sample jump through a 100 ms preset recall and a per-sample 500→6000 Hz sweep — click-free is asserted, not assumed.
  • Preset morph in the kernel. 16 slots; recall_preset is ramp_to on all six parameters with a shared duration — as safe as any motion.
  • Anti-denormal on the stage states (anti_denormal, the tap.comb~ 1e-15 idiom), inside tpt — a ringing-out filter otherwise decays into denormals and multiplies its own CPU cost.
  • Newton is guarded, not trusted. Seed clamp, derivative fallback, iteration cap: solve_exact cannot NaN or hang, only degrade toward solver_fast.
  • Allocation-free after prepare(); setters are plain stores into ramp targets, safe from the message thread while audio runs.

Checkpoint

Four trapezoidal one-poles put −180° and gain 1/4 at the prewarped cutoff; negative feedback makes k = 4 the oscillation threshold, which is why resonance is calibrated in quarters of k and self-oscillation lands on pitch (8009 Hz for 8 kHz, measured). The linear loop solves in closed form; the saturating loop is predicted linearly and committed through the tanh stages once — exact in the small-signal limit, backstopped by a guarded Newton solver. asym shifts the tanh operating point, comp pre-multiplies away the derived 1/(1+k) droop, the Xpander table is binomial algebra over the taps, and the oversampling chain pays tanh's alias bill with a measured 13.5 dB. Every number in the user-facing chapter traces to a line in this file.

The master phase and its corrections: vco.h

The user-facing chapter promised an oscillator whose folded harmonics sit ~47 dB down, whose analog section is "exactly zero by default" with a seed that works like a serial number, and whose FM survives through zero. Each claim is a theorem about this file or a measurement of it. This appendix derives the corrections — polyBLEP, the leaky triangle, the sync patch — then the analog-character section, including the one place where honest analysis contradicted intuition and the tests were written to match.

One phase, many readings

There is a single accumulator, m_phase ∈ [0,1), advanced once per sample in step:

f_eff = base_hz · 2^(cents/1200) + fm_hz      (pitch is exponential,
dt    = f_eff / m_sr,  clamped to ±0.49        FM is linear, in Hz)
adt   = max(|dt|, 1e-8)

cents collects detune, drift, jitter, the per-unit tolerance offset, and track — everything musical multiplies; only FM adds. Every waveform is a reading of the same phase: sine through sin, saw as 2p−1, pulse as a comparison against pw, triangle as an integral. The shape morph crossfades adjacent readings of one phase, so it can never produce a discontinuity the phase itself doesn't have. The problem is entirely the discontinuities.

The VCO as a diagram: frequency sum into the master phase accumulator, fanning to the four waveform readings and the shape crossfade

The fan-out the chapter title promises: every waveform is a reading of the same φ.

The residual: deriving poly_blep

A naive saw jumps by −2 at the wrap; a step's spectrum falls at only 6 dB/oct, so its harmonics march past Nyquist and fold back inharmonic. The ideal fix is a band-limited step — the integral of a sinc. The polyBLEP observation: the band-limited step differs from the naive step only near the edge, so instead of storing sinc-integral tables (minBLEP), approximate the difference with a polynomial. Take the crudest kernel one sample wide per side — a unit-area triangle, b(τ) = 1 − |τ| on τ ∈ [−1, 1] with τ in samples from the discontinuity — integrate, subtract the step:

s_bl(τ) = (τ+1)²/2             τ ∈ [−1, 0]
s_bl(τ) = 1 − (1−τ)²/2         τ ∈ [0, 1]

r(τ) = s_bl − s_naive:
r(τ) = (τ+1)²/2                τ ∈ [−1, 0)      (before the edge)
r(τ) = −(1−τ)²/2               τ ∈ [0, 1]       (after the edge)

Scale by 2 — the saw's wrap step — and these are exactly the file's poly_blep branches: just after the wrap (t < dt, normalized t/dt = τ), t + t − t*t − 1 = −(1−τ)² = 2·r; just before it (t > 1.0 − dt, normalized (t−1)/dt = τ ∈ [−1,0)), t*t + t + t + 1 = (τ+1)² = 2·r. Three properties fall out of the derivation:

  • Continuity at the window edges. r(−1) = r(1) = 0: the correction fades in and out without new discontinuities of its own.
  • The midpoint property. r(0⁻) = +½, r(0⁺) = −½: the corrected edge passes through the middle of the jump, as a true band-limited step does.
  • "±1 sample" precisely. A sample lands in a branch iff its phase is within dt of the wrap, and phase moves dt per sample — exactly the last sample before and the first after each edge are touched, ever; the scope trace still looks like a saw.

The triangle kernel approximates the sinc (its spectrum is sinc², not a brickwall), so suppression is finite and measurable: the notebook drives a 3951 Hz saw, whose 13th harmonic folds to 3364 Hz, and measures it at −26.7 dB naive versus −74.2 dB with polyBLEP — 47.5 dB of suppression. The notebook's sample-level zoom shows the mechanism: the corrected saw passes through +0.588 and −0.856 on its way down, where the naive saw jumps in a single step.

Saw, pulse, and the second BLEP

saw_at is the reading minus the residual: 2·bent(p,·) − 1 − poly_blep(p, dt) (subtracted: the wrap step is −2, poly_blep is normalized for +2). pulse_at is ±1 with two edges: the rising edge at the wrap (step +2, residual added) and the falling edge at p = pw (step −2, residual subtracted) — the latter evaluated at wrap01(p − pw), re-centering the phase coordinate so that edge sits at zero of its own window and the same branches apply. Calibration is pinned by measurement: a bipolar pulse at duty d must average 2d − 1, and the notebook measures −0.800 / −0.500 / +0.000 at 10 / 25 / 50 %; the kernel test holds the 25 % mean within ±0.03.

The triangle: integrate, but leak

A triangle is the integral of a square — the classic analog trick. A ±1 square at frequency f forces slope ±4f (2 units in half a period 1/(2f)), so the per-sample increment is ±4·f/fs = ±4·dt, which is tri_tick's scaling exactly:

m_tri_state = 0.999 · m_tri_state + 4.0 · adt · sq

giving peak ±1 with no post-normalization. The 0.999 is the honesty tax: the BLEP-corrected square's samples do not sum to exactly zero per period (the two edges land at different sub-sample positions, so their corrections don't cancel — and tri_pw skew under imperfect makes the imbalance deliberate), and a pure integrator would ramp that residue to infinity. The leak turns the integrator into a one-pole highpass with corner fs·(1 − 0.999)/2π ≈ 7.6 Hz at 48 kHz — far below any audible fundamental, high enough to hold DC bounded.

Why the integrated square is correctly antialiased: integration multiplies the spectrum by 1/ω, −6 dB/oct. The square's already-suppressed alias residual was generated near Nyquist, where 1/ω is smallest, so integrating a BLEP square improves the alias-to-harmonic ratio — the correction gets cheaper exactly where the waveform gets harder, which is why this hardware trick survives digitally intact.

Through-zero FM

Because FM adds in Hz after the exponential pitch math, dt can go negative and the phase genuinely runs backward — that is all "through zero" means, and why the sidebands stay coherent when the modulation swings past the carrier. Two guards make it safe. The BLEP windows use adt = |dt|: a window is a duration, one sample each side of an edge, whichever way the phase travels (with a 1e-8 floor so the t /= dt normalization survives a frozen phase). And dt is clamped to ±0.49: at |dt| ≥ 0.5 the window tests t < dt and t > 1 − dt would overlap and every sample would be "at an edge" — the clamp keeps the effective frequency below Nyquist, where the model means anything at all. Measured: a 500 Hz sine under ±900 Hz of FM at a 100 Hz rate — depth past the carrier, genuinely through zero — puts its sideband at −13.7 dB with −158.3 dB between the lines (144.5 dB of contrast), bounded at |y| = 1.00.

Hard sync, one-sided

A rising zero crossing on the sync input (m_sync_prev ≤ 0, sync > 0) resets the phase. Linear interpolation locates the crossing inside the sample:

frac    = m_sync_prev / (m_sync_prev − sync)      ∈ [0, 1)
m_phase = wrap01((1.0 − frac) · dt)

— the phase restarts from zero at the crossing and accumulates only the remaining fraction of the sample, so sync pitch is sub-sample accurate (the notebook's synced slave measures periodic at 110.1 Hz against a 110 Hz master). The reset is still a discontinuity of size d = waveform_out_peek(p_old, …) − waveform_out_peek(wrap01(p_new), …), sized on the morphed waveform without advancing the triangle integrator:

x = 1.0 − frac        correction += d · 0.5 · x²

This is a first-order polynomial BLEP, honestly cruder than the saw's: one-sided, because the pre-reset sample is already output when the edge arrives — a reset cannot be predicted — and a one-sided patch can never reproduce the full band-limited edge (the midpoint property needed both sides). d·½x² is the triangle-kernel residual for a step landing x into a sample; the code feeds it x = 1 − frac, the elapsed fraction since the crossing, so its weighting runs opposite to the two-point post branch (½·frac²) — at the first-order accuracy a one-sided patch can claim, both are O(d) click reducers vanishing at one end of the window. The header flags minBLEP tables as the wholesale upgrade; a m_pending slot, read and cleared each sample but never written, is scaffolding for the second correction sample it would need.

The analog section, derived (2026-07)

Two time scales of pitch noise. tick_drift is sample-and-hold noise redrawn every m_sr / 2 samples (~2 Hz) smoothed by a one-pole with a = 1 − exp(−2π·0.5/m_sr) — the exact discrete step of a 0.5 Hz lowpass. tick_jitter is the same structure at ~80 Hz through ~40 Hz: the fast companion, trembling where drift strolls. Both are depth-scaled in cents into the pitch path. Measured: relative period spread 2.01×10⁻⁷ at jitter 0 versus 2.74×10⁻³ at 10 cents — four orders of magnitude of micro-instability, still under the test's 0.02 ceiling.

The bent ramp, honestly. imperfect bows the saw via bent(p, bend) = p + bend·p·(1−p), bend = 0.35·imp·m_tol_curve. The parabola vanishes at both endpoints, so the wrap step stays exactly 2 and the BLEP stays correctly sized — why the bend lives inside the ramp reading. Now the honest part. In x = p − ½ the saw 2p−1 = 2x is odd (a sine series) while the parabola p(1−p) = ¼ − x² is even (a cosine series): the bend's Fourier content is in quadrature with the saw's own components. Harmonic k gains an orthogonal part of relative size bend/(πk) that moves its magnitude only at second order — about 0.05 dB at k = 1 for the maximal bend of 0.35, per √(1 + (bend/πk)²): a scope-obvious shark fin, almost no harmonic-magnitude shift. Discovered by measurement — and the kernel test matches the truth: it asserts the waveform bow (interior deviation > 0.03 at imperfect 1) and makes no harmonic-magnitude claim.

Where the spectral work is actually done. The reset corner rounds through a one-pole whose cutoff closes from ~22 kHz toward ~8 kHz (fc = min(22000 − 14000·imp, 0.45·m_sr), coefficient cached against m_round_imp): measured, the saw's 40th harmonic (17.6 kHz) is 6.4 dB quieter at imperfect 1; the test requires > 4 dB. The triangle skews via tri_pw = clamp(0.5 + imp·m_tol_tri·0.01, 0.05, 0.95) — duty asymmetry in the integrated square is even harmonics — measured: triangle h2 rises from −185.5 dB (numerically absent) to −34.1 dB at imperfect 0.8. The sine reads a mildly bent phase (bent(p, 0.5·bend)), the pulse width takes a static offset up to ±1.5 %, the whole unit a pitch offset up to ±2 cents (m_tol_cents).

Which unit you own. The tolerances come from a separate stream: compute_tolerances hashes the seed (m_seed * 2654435761u + 12345u) into its own local LCG, never touching the runtime m_rng. The contract: clear() resets m_rng = m_seed and all noise state but does not re-roll tolerances — resetting the oscillator must never change which unit off the production line you own; only set_seed re-rolls, because changing the serial number is changing the unit. Every tolerance is scaled by imperfect at use, yielding the contract the section rests on: at imperfect 0, every seed is bit-identical to the ideal oscillator. The kernel test renders seeds 7 and 8 and requires ya == yb — exact equality over 24000 samples — and the notebook confirms it; conversely, with drift 20 seeds 7 and 8 diverge by up to 0.183 (measured) while the same seed renders bit-identically, and the test pins that at imperfect 0.6 different seeds are audibly different units.

track.

cents += track · log₂(base_hz / 440.0)

A V/oct converter's calibration error grows linearly in octaves from its trim point; this is that line, exact at 440 Hz by construction (log₂ 1 = 0). Measured at track 5: −15.0 / −10.0 / −5.0 / +0.0 / +5.0 / +10.0 / +15.0 cents across −3…+3 octaves; the test holds the trim point under 1 cent, ±3 octaves within 2 cents of ±15.

The engineering ledger

  • Determinism is structural. All randomness flows from one 32-bit LCG (1664525 / 1013904223) seeded by m_seed (0 remapped to 1); no wall clock, no std::random — renders, tests, and mc. stacks reproduce bit-for-bit, and the jitter test pins same-seed bit-identity.
  • Off means exactly off. tick_drift/tick_jitter return before consuming the RNG at zero depth — a default-configured oscillator never advances m_rng, so different seeds render identically until a stochastic feature is engaged. The corner-rounding pole is gated on imp > 0.0, its state primed while bypassed (m_round_lp = y) so engaging imperfect mid-note is click-free.
  • The triangle integrator ticks only when the morph needs it. waveform_out short-circuits the crossfade endpoints (a <= 0.0), and tri_tick is stateful — skipping it when unused is a cost saving and a correctness rule (parking at pure saw must not silently integrate); sync sizing uses waveform_out_peek for the same reason.
  • Clamps with reasons: dt at ±0.49 (window overlap / Nyquist), adt floored at 1e-8 (division in poly_blep), tri_pw in [0.05, 0.95] and pw in [0.01, 0.99] (an edge pair must stay two distinct windows).
  • The house frame: per-sample linear ramps with an active-count fast path, 16 preset slots morphable over time, allocation-free processing, setters safe while audio runs — the same bones as ladder.h and svf.h, so the wrapper stays a shim.

Checkpoint

One master phase; every waveform is a reading of it, and every reading's discontinuity gets the residual of a triangle-kernel band-limited step — two samples per edge, continuous at its window boundaries, a measured 47.5 dB of alias suppression at the folded 13th harmonic. The triangle integrates the corrected square (slope ±4f, hence 4·adt) with a 0.999 leak; FM adds in Hz so the phase can run backward, adt keeping the windows directionless; sync resets with sub-sample accuracy and patches the step one-sidedly because resets can't be predicted — minBLEP is the flagged upgrade. The analog section is derived noise at two time scales, a quadrature-honest bent ramp, a rounding pole, a skewed duty, and a calibration line exact at A440 — all drawn from a tolerance stream clear() never touches, all scaled by imperfect, and all provably absent at zero: the ideal oscillator is a test-pinned invariant, not a default setting.

Detector, law, and a borrowed filter: autowah.h

The user-facing chapter (The pedal that listens) made three measurable claims: the sweep law matches its design to 0.000 cents, the follower's timing is an honest RC discharge, and sensitivity at the floor turns the object into a truly fixed filter. This appendix derives each, starting from the decision that tap.autowah~ is mostly not a new filter.

The composition decision: don't write a second SVF

The Snow White's core is an LM13700 OTA state-variable filter swept by an envelope. TapTools already ships an SVF kernel whose defining property — proved in the SVF appendix — is A-stability under per-sample cutoff modulation. An envelope-swept filter moves its cutoff every sample by construction; the property the wah needs most is exactly the one svf.h already guarantees by theorem. So wah_filter owns a tap::tools::svf::svf_filter member (m_svf) and drives it through the signal-rate path, m_svf.tick(m_cutoff) then m_svf.process(0, x), once per sample. The house rule makes this legal: objects under source/projects/ stay self-contained, but inside the kernel repo sharing between kernels is encouraged — autowah.h simply #include "svf.h".

Composition buys something subtler than saved code: the corner-identity argument. When the envelope is off, the wah is a bare SVF, and that is a testable equation rather than a resemblance. The kernel test ("sensitivity at the floor is the cocked-wah: bit-close to a bare svf at bias") runs the wah at sensitivity −60 dB, bias 800 Hz, resonance 0.7 against a separately constructed svf::svf_filter fed ref.process(x, 800.0), and requires maxerr < 1e-12 over half a second of signal. The wet paths are arithmetic-identical — same tick(cutoff) entry, same clamps, same solve — so why 10⁻¹² and not ==? The dry leak: at mix = 100 the mix angle is θ = π/2, and while sin(π/2) is exactly 1.0 in doubles, cos(π/2) rounds to 6.123×10⁻¹⁷, so the output carries a 6×10⁻¹⁷·x dry residue. The tolerance covers one ulp-scale cosine, nothing else. Resonance meaning is shared the same way — the wah's 0..1 knob goes through the SVF's own q_from_resonance mapping, so "resonance 0.7" means the same Q in both objects.

The autowah composition as a diagram: detector chain and sweep law around the boxed borrowed svf_filter member

The composition decision, drawn: everything amber is this file; the blue box is svf.h, borrowed intact.

The detector: gain, rectifier, follower

The detector chain in process() is three lines:

driven = key · m_sens_gain                           (dB → linear input gain)
rect   = |driven|            (full-wave, default)
       or max(driven, 0)     (half-wave, the traced single-diode topology)
m_env += coef · (rect − m_env)                       (one-pole follower)

The sensitivity floor is a contract, not a clamp. update_derived maps the dB knob as

m_sens_gain = 0                    if sens_db ≤ −60 dB
            = 10^(sens_db / 20)    otherwise

−60 dB is not "very quiet" (that would be gain 0.001); it is exactly zero. With m_sens_gain = 0 the rectifier output is identically 0, m_env decays to 0 and stays there, m_sweep = tanh(0) = 0, and map_cutoff(0) returns m_bias exactly — the test asserts w.cutoff_hz() == 250.0 with ==. That is what makes the pedal's secondary "cocked wah" mode a true fixed filter rather than an approximately-fixed one that still breathes a few cents with the input. Factory slot 3 is that voicing as data.

The follower coefficient is the exact RC discretization. The analog detector is a capacitor charged toward the rectified signal: env′ = (rect − env)/τ. Solving that ODE exactly over one sample period T = 1/fs gives env[n] = rect + (env[n−1] − rect)·e^(−T/τ), which rearranges to the code's recurrence with

a = 1 − e^(−1/(τ·fs))        (the file: m_attack_coef = 1 − exp(−1000/(ms·m_sr)))

After N = τ·fs samples of a step, the remaining error is (1−a)^N = e^(−N/(τ·fs)) = e^(−1): the envelope reaches 63.2% in exactly τ. This is not the cheap approximation a ≈ 1/(τ·fs); the exponential form makes the ms parameters honest at any rate. The notebook measured it: attack set to 2.0 ms reaches 63% in 1.94 ms; decay set to 250 ms falls to 36.8% in 256 ms; and a log-domain fit of the release is a pure exponential with τ = 252 ms and residual σ = 0.004 — an RC discharge, like the hardware. The attack/release asymmetry is one branch, coef = (rect > m_env) ? m_attack_coef : m_decay_coef — the diode charges the cap through one resistance and lets it bleed through another.

Full-wave default, half-wave option. The traced hardware detector is a single diode: it charges only on positive half-cycles, so the envelope droops between charges at the signal's fundamental. Full-wave rectification charges twice per cycle and halves the gaps. The follower cannot filter this out without also slowing the response, so the ripple rides the envelope and frequency-modulates the cutoff — the hardware's "sweep-rate ripple." The notebook quantified the A/B on a 110 Hz tone (decay 60 ms): settled envelope ripple (std/mean) 0.7% full-wave vs 2.6% half-wave. The kernel defaults to the cleaner full-wave and keeps half-wave selectable (set_rectifier()) because the flavor question is a hardware-listening question, not a math question — it waits for the calibration pass.

The sweep law, and where it is honest about ignorance

m_sweep  = tanh(k_env_knee · m_env)                     k_env_knee = 1.5
m_cutoff = m_bias · 2^(m_sweep · m_range)               clamped to [20 Hz, min(20 kHz, 0.45·fs)]

Three deliberate choices:

(a) Exponential in Hz. Equal envelope increments move the cutoff by equal octaves, which is how a sweep sounds uniform — pitch perception is log-frequency. The honest caveat lives in the header and in map_cutoff()'s own comment: the LM13700's frequency is linear in control current, so the pedal's true law hinges on the BJT stage that converts the envelope voltage into that current — plausibly exponential (a BJT's collector current is exponential in V_BE), but not yet measured. That is why the law is one isolated function: if calibration finds a linear V→I driver, map_cutoff becomes bias + sweep · span_hz and nothing else in the kernel changes.

(b) The tanh soft knee. Without it, hard playing would pin sweep at a clamp rail — a hard corner in the control trajectory, audible as the filter slamming its ceiling. With it, the ceiling is approached asymptotically. The arithmetic at the defaults (bias 250 Hz, range 3.3 octaves, sensitivity 0 dB):

full-scale DC key → m_env → 1
m_sweep → tanh(1.5) = 0.905
m_cutoff → 250 · 2^(0.905 · 3.3) ≈ 1982 Hz

— about 2 kHz, under the asymptotic rail 250·2^3.3 ≈ 2462 Hz, which itself matches the hardware's published 250–2500 Hz span. The unit test pins the settled cutoff into (1800, 2100) Hz and separately drives an absurd +24 dB sensitivity into an 8× full-scale key to confirm the cutoff saturates at the ceiling (reaching > 99% of it) instead of running away. range is signed — negative sweeps down from bias, a deliberate extension the pedal never had.

(c) Measured. The validation notebook swept the envelope range and compared measured cutoff against the designed curve: max error 0.000 cents. The law in the code is the law on paper; when hardware recordings arrive, any disagreement is a fact about the model choice, not about the implementation.

One filter, two owners: the forwarding discipline

Both kernels ship per-sample parameter ramps. Run both and every set would be smoothed twice — lagged, and worse, shaped (a ramp of a ramp is not a ramp). So prepare() declares a single owner:

m_svf.set_smooth_ms(0.0);   // this kernel owns all smoothing; svf setters snap

The wah's own ramp array smooths every audible parameter; the composed SVF's setters snap instantly to whatever the wah forwards. And forwarding is change-gated: update_derived pushes set_resonance / set_drive / set_circuit only when the cached values (m_svf_resonance, m_svf_drive, m_svf_circuit, seeded to −1 to force the first forward) actually differ. The reason is the SVF's two-tier update from its own appendix: those setters dirty the shape tier (damping, mix weights, drive gain — a pow and the mix logic). While any wah ramp is active, update_derived runs every sample; forwarding unconditionally would re-run the SVF's shape update every sample of every bias morph even though resonance never moved. Cutoff needs no gate at all — tick(cutoff_hz) lands in the SVF's cutoff cache, which recomputes the tan and solve constants only when the value differs.

The circuit switch

drive at 0 dB runs the SVF's clean linear circuit; anything above engages circuit_driven — tanh band-node limiting, 2× oversampled — as the optional OTA-flavored color stage. The switch is a threshold in update_derived:

circuit = (m_svf_drive > 1e-6) ? circuit_driven : circuit_clean

Why is switching circuits mid-stream acceptable? At the switch point drive ≈ 0 dB, so the driven circuit's input gain is 1 and tanh is near-identity at typical band-node levels — the two circuits compute nearly the same output, and the transition is benign. Honestly stated: near-identity, not identity. At high resonance the band node runs hot, tanh visibly bends, and the driven circuit also brings its oversampling path with it — so engaging drive from zero on a screaming resonant setting is a small audible step. The abuse test accepts this trade explicitly: resonance 1.0 plus max drive on square-wave bursts must stay bounded and finite (the SVF's bounded self-oscillation doing its job), not polite.

Output staging is the equal-power crossfade shared with tap.crossfade~: θ = mix·π/200, m_dry_gain = cos θ · g, m_wet_gain = sin θ · g — the master gain g rides both paths, so gain never changes the balance.

The engineering ledger

  • Per-sample cost accounting. Settled steady state pays: one rectify, one follower multiply-add, one tanh (knee), one exp2 (law), the SVF solve, two mix multiplies. The pow(10, ·)/exp calls live only in update_derived, which runs per sample while ramps move and exactly once after they settle (m_derived_dirty re-arms only when m_ramps_active > 0). The real recurring cost is the SVF's cutoff-tier tan, paid whenever the envelope actually moved the cutoff — and skipped by the SVF's cutoff cache whenever it didn't (silence, or the cocked wah).
  • envelope() and cutoff_hz() exist for measurement. They read m_sweep and m_cutoff after the fact; the C ABI's taptools_wah_process(..., env_out, cutoff_out, n) taps them per sample, and the validation notebook's trace=True path — the ground truth its STFT peak-trajectory extractor was proven against (0.979 log-frequency correlation) — is built on exactly these two accessors.
  • The preset-morph engine is the GRM pattern (16 slots, the grm_comb/grm_pitchaccum house count): store_preset captures ramp targets (knob positions, never mid-ramp instantaneous values), recall_preset(slot, seconds) re-targets every ramp so a morph is just nine simultaneous ramps — re-targeting mid-morph stays continuous for free. The test walks a 100 ms morph and requires bias to move monotonically with no step larger than one ramp increment.
  • Factory slots are data, not code: four params structs (guitar / bass / slow swell / cocked wah) in slots 0–3. Changing a voicing after the hardware session edits numbers, not logic; the test pins them.
  • Structural switches (mode, rectifier) are not ramped or morphed — interpolating between rectifier topologies has no physical meaning.
  • Anti-denormal on the envelope (< 1e-15 → 0, the tap.comb~ idiom): a decaying exponential otherwise glides into denormals and multiplies its CPU cost during silence.
  • Sidechain by signature: process(x) is process(x, x); the wrapper's key inlet is the two-argument form. Single-channel by design — per-channel envelopes are the correct behavior under mc. wrapping.

The calibration pass, by construction

Every open hardware question maps to one isolated switch point: the sweep law is map_cutoff() (one function), the stock filter tap is m_mode's default (mode_lowpass, flagged in the header as inference), the detector topology is set_rectifier(), the knee is k_env_knee (one constant). The validation notebook is the instrument that will close them: its extractor recovers the swept peak from wet audio alone, is already calibrated against the kernel's own trajectories, and its last cell waits for snowwhite_*.wav. When the pedal arrives, disagreements land on named constants — not on a rewrite.

Checkpoint

The wah is a detector and a law in front of a borrowed filter. Composing svf_filter puts the per-sample-modulation stability where it is already proven, and makes "sensitivity off equals a bare SVF" an identity checked to 10⁻¹² (the gap being one rounded cosine). The follower coefficient 1 − e^(−1/(τ·fs)) is the exact RC step — 63.2% in τ by algebra, 1.94 ms measured for 2.0 set. The sweep law is exponential-in-Hz through a tanh knee that turns hard playing into asymptotic approach (~2 kHz at the defaults, under the hardware's 2.5 kHz rail) — measured at 0.000 cents against design, and honestly provisional, isolated in one function until the real pedal votes.

Convolution without compromise: conv_engine.h

The user-facing chapter (Borrowed rooms) made a flat claim: tap.convolve~ is exact — not "high quality," exact — and its only cost is a latency of precisely blocksize samples. Claims like that are either algebra or advertising. This appendix does the algebra: why the convolution is partitioned at all, why overlap-save, why the latency is exactly one partition, and how an impulse response can be replaced mid-performance without the audio thread ever seeing a torn table.

Why partitioned: the cost triangle

Direct convolution of a stereo pair against an L-second IR at rate fs costs

cost_direct = L·fs MACs per output sample per path

— 48 000 multiply-adds per sample for one second of room at 48 kHz, times four paths for true stereo. Untenable. The classical fix is to convolve in the frequency domain: transform the whole IR once, multiply spectra, invert. But a single-FFT scheme cannot emit anything until it holds a full frame of input, so its latency equals the IR length — seconds of delay for a reverb. Also untenable.

Uniform partitioning takes the middle of the triangle. Split the IR into P equal partitions of B samples, h_j[k] = h[j·B + k]; then by linearity

h = Σ_j h_j delayed by j·B

Each partition is short enough that its convolution can be computed with a small FFT once per B-sample block, and the delays j·B are whole blocks — which, we will see, cost nothing but indexing. FFT economics, latency of one partition. This is the standard engine of the genre for a reason.

Overlap-save: the framing, derived

The engine convolves each partition by circular convolution over an FFT of size m_fftsize = 2·m_block — size 2B for partition size B. Circular and linear convolution are not the same thing; the design question is which output samples of the circular product are also the linear ones.

Take the frame the code actually builds in process_block():

frame_m = [ block_{m−1} ; block_m ]        (m_fre[j] = m_prev[ch][j]; m_fre[B+j] = m_inblk[ch][j])

and a partition h zero-padded from B to 2B. The circular convolution is

y_circ[n] = Σ_{k=0}^{B−1} h[k] · frame[(n − k) mod 2B]

For n in the second half, n ∈ [B, 2B), and k < B, the index n − k stays in [1, 2B): the mod never wraps, so y_circ[n] equals the linear convolution of h with the input stream at that time. For n < B the mod does wrap, splicing in samples from the frame's far end — time aliasing. So each 2B-point product yields exactly B valid samples, the second half, and the code keeps precisely those:

m_outblk[oc][j] = m_are[m_block + j];   // overlap-save: discard the aliased first half

That is the whole scheme: hop by B, keep the clean half, discard the dirty half. The alternative, overlap-add, zero-pads each input block instead and sums overlapping output tails — equally exact in theory, but it carries a partial-sum accumulation buffer across block boundaries, one more piece of state to get right. Overlap-save's output block is finished the moment the IFFT returns: no summation state, no output windowing, no crossfading — there is nothing between the IFFT and the output buffer that could be inexact. Every step in the chain — framing (a copy), FFT and IFFT (the shared radix-2 in fft.h, whose inverse divides by N so a round trip reconstructs its input), and the multiply-accumulate — is exact linear algebra in double precision. The engine has no tuning parameters that trade accuracy for speed; its error budget is rounding noise, and the measurements below confirm that is all there is.

The frequency-domain delay line

The FDL as a diagram: the ring of past input spectra multiplied per bin against the partition spectra, accumulated, and inverse-transformed with the aliased half discarded

Why FFT cost is constant in IR length: the transforms bracket the structure, and only the MAC sees the partitions.

The delays j·B remain. Delaying partition j's contribution by j blocks is the same as convolving it with the input from j blocks ago — and the frame for block m − j has already been transformed. So the engine keeps a ring of past input spectra (the FDL, m_fdl_re/m_fdl_im, one per input channel) and forms the output spectrum as

Y_m[k] = Σ_{j=0}^{P−1} H_j[k] · X_{m−j}[k]

which is exactly the inner loop: slot = cur − p (mod m_max_parts), then a complex multiply-accumulate over all m_fftsize bins into m_are/m_aim. The consequence for cost is the design's payoff: one forward FFT per input channel and one inverse FFT per output channel per block, regardless of P. Growing the IR grows only the MAC. Per output sample, the MAC costs P·2B complex MACs / B samples = 2P complex MACs ≈ 8P real multiplies per path, versus P·B real MACs for direct convolution — a factor of B/8 (64× at B = 512), with the FFTs an O(log B) constant on top.

Latency is exactly B, by the framing

Follow one sample through process(): it is written into m_inblk at position m_pos, and the output handed back at that same call is read from m_outblk[m_pos] — a block computed when the previous block completed. When block m finishes, process_block() runs with a frame ending at the newest sample, and its B valid outputs are the linear convolution up to that sample; they are then dealt out during block m + 1. So output sample t carries y(t − B): the first partition's contribution to a block is computed from the block just gathered, not from anything older, and the delay is one partition — no more (the frame includes the newest sample) and no less (nothing can be emitted before a block is complete). The unit test pins both edges: the first B output samples are silence (the pre-roll), and an IR of δ at index 5 yields the input delayed by exactly B + 5. The notebook verified the latency at B = 64, 256, and 1024 — always exactly B.

Exact, measured

The notebook (executing the real engine through the C ABI) puts numbers on "exact": against a direct time-domain convolution of the same IR — 24 000 samples, B = 512, 47 partitions — the maximum difference is 3.13×10⁻¹². Across block sizes the outputs agree with the direct reference to 2.27×10⁻¹³ / 1.17×10⁻¹² / 1.76×10⁻¹² (B = 64/256/1024), and with each other, latency-removed, to 1.81×10⁻¹² — the block size is a CPU/latency dial with no audible existence. An impulse through the engine reconstructs the loaded IR to 5.5×10⁻¹⁴, and a synthetic 0.60 s-RT60 reverb measures back at 0.599 s. The unit test does the same job in CI with independent per-path IRs, deliberately awkward 10-sample process chunks that straddle block boundaries, and 10⁻⁹ tolerances.

True stereo: four paths, two FFTs

A stereo room is a 2×2 linear system, and the engine runs all of it:

out_l = in_l ∗ h_LL + in_r ∗ h_RL ;   out_r = in_l ∗ h_LR + in_r ∗ h_RR

with path = in_channel·2 + out_channel (0 = LL, 1 = LR, 2 = RL, 3 = RR). The economics are better than 4× mono: the two input FFTs are shared across all four paths, and each output channel needs one inverse — so a block costs 2 forward FFTs, 2 inverse FFTs, and 4 MAC passes. The notebook pins the routing: an impulse into L only emerges on R at exactly the cross-feed path's gain (0.600 expected, 0.600 measured) and, with the off-diagonal paths silent, R stays at 0.0 — cross-terms cannot hide in each other. The mapping from buffer~ channel count to paths (4+ = true stereo, 2 = dual mono, 1 = same room both sides) is wrapper policy; the engine only ever knows four pointers, any of which may be null for a silent path.

The atomic IR swap

Loading a room while the music plays is the one place this engine touches concurrency, and it is confined to a single atomic. The IR tables are double-buffered per path (m_ir_re[path][slot], slots 0/1). load_ir() — which runs off the audio thread; it is the expensive part, P analysis FFTs per path — writes only the inactive slot, then publishes:

m_slot_parts[inactive] = P;                          // written before the publish...
m_active.store(inactive, std::memory_order_release); // ...so (slot, P) stay consistent.

The perform loop does one acquire load of m_active per block. The release/acquire pair means that if the audio thread observes the new slot index, it also observes that slot's fully written spectra and its partition count — slot and P travel through one atomic, so there is no window where the loop MACs over half-written tables or over the wrong number of partitions. Until the store, the loop reads the old slot, which the loader never touches. The discipline is single-writer double-buffering: publishes are serialized through the wrapper's message path, and the just vacated slot is only rewritten by the next load.

Why does a swap settle in exactly one block? Because the FDL stores input spectra, not output. The first process_block() after the publish already renders the entire tail — all P partitions — from the new IR against the existing input history; the only samples that differ from a new-IR-from-the-start engine are the ones sitting in m_outblk, computed just before the swap. The unit test pins the settling (output equals the new IR's pure delay from shortly after the swap); the notebook pins it exactly: max |swapped − reference| after +1 block = 0.00×10⁰ — bit-identical to an engine that had the new IR from the start — with RMS continuity across the swap instant, 21.922 before, 22.147 just after, no dropout. Honest limit: the swap is a hard splice between two exact convolutions, click-free but discrete; the engine does not interpolate between rooms.

A related freebie of the input-side FDL: analysis of incoming audio happens before the has-IR check, so the delay line is warm even while no IR is loaded — load the first room mid-stream and its tail renders immediately from audio already played.

The engineering ledger

  • Uniform, not Gardner non-uniform, partitioning. Non-uniform schemes (short partitions first, growing later) can push latency below B for the same CPU, but they need multiple FFT sizes and a scheduler that spreads long-partition work across blocks — real complexity with real failure modes. The object's latency budget is satisfied by making B small (64 samples = 1.3 ms, verified exact above); complexity was not bought that nothing needed.
  • The shared FFT. fft.h is the in-house radix-2 Cooley–Tukey used by the whole spectral set; per its header it lived byte-identical inside conv_engine, tap.nr~, and tap.spectra~ before being consolidated at the kernel split. In-place, forward unscaled, inverse divides by N — round-trip exact by construction. No external FFT dependency, per the porting philosophy.
  • IR stored as float32, deliberately. A buffer~ holds 32-bit samples; the engine quantizes at load_ir (static_cast<double>(src[idx]) * scale) and computes in doubles thereafter. The notebook's direct-convolution reference casts its IR through float32 the same way — so the 10⁻¹² figures isolate the algorithm, not the source quantization the wrapper inherits from Max regardless.
  • Geometry only in configure(). Partition size and capacity determine every buffer, so reallocation happens only where the audio thread is idle — the wrapper calls it from dspsetup. clear() flushes running state with std::fill only, no reallocation, and is safe from a message handler; process() allocates nothing, ever (scratch spectra m_fre/m_fim/m_are/m_aim are preallocated and reused).
  • Capacity vs. length. The FDL ring is sized m_max_parts; a loaded IR uses P ≤ m_max_parts partitions (load_ir clamps), and the MAC runs over P only — a short room in a big engine costs a short room.
  • The deferred optimization, on the record. The spectra are stored and MAC'd full-complex; the input is real, so a half-spectrum (N/2 + 1 bins, Hermitian symmetry) form would halve both the MAC and the IR/FDL memory. The header flags it and parks it, under the same house rule the SVF appendix recorded: optimizations land bit-identical or explicitly signed off — and re-deriving the packing arithmetic is exactly the kind of change that gets signed off with a measurement, not slipped in.

Checkpoint

Partitioning splits the IR by linearity; overlap-save framing makes each 2B-point circular product yield B exactly-linear samples with nothing to window or crossfade; the frequency-domain delay line turns partition delays into ring indexing, so FFT cost is constant in IR length and only the MAC grows. Latency is one partition by the framing — measured at exactly B for every B tried — and exactness is measured at 10⁻¹²-and-below everywhere it can be probed. The one concurrent act, swapping rooms, rides a single release/acquire atomic over double-buffered tables, and settles bit-identically in one block because the delay line remembers input, not output. The compromises the genre usually accepts — approximate tails, block-size coloration, swap dropouts — are absent, and the measurements say so.

Ring time as the truth: grm_comb.h

The user-facing chapter made three claims that sound like marketing until you do the math: that a voice keeps its decay as its pitch sweeps, that warp stretches the partials while the fundamental stays in tune, and that phase at 100 cancels the even harmonics — exactly. This appendix derives all three the way the file was designed, then walks the code-level decisions. The behavioral claims below are pinned by the kernel scenarios in tap.5comb_tilde_test.cpp, which drive tap::tools::fivecomb::comb_bank directly (no Max in the loop); the few numbers outside the test suite are marked as measured on the kernel for this chapter.

A comb is a string

One voice is a delay of d samples fed back on itself. Ignoring the in-loop filters for a moment, the recursion the code implements (y = in + fb * ap_out, written back into the delay line) is:

y[n] = x[n] + fb · y[n − d]        h[n] = δ[n] + fb·δ[n−d] + fb²·δ[n−2d] + …

The impulse response is echoes every d samples, each scaled by another factor of fb — and echoes every 1/f seconds is a tone at f and its harmonics: a plucked string tuned to f = fs/d. The first kernel scenario pins the geometry: a 500 Hz voice at 48 kHz (d ≈ 96) puts its first three echoes at one period spacing, within a couple of samples of 96/192/288.

Ring time as the truth

The legacy object exposed fb directly. This file refuses to, and the reason is in the impulse response above. After t seconds the signal has made t·fs/d round trips, so the decay envelope is level(t) = fb^(t·fs/d).

Reverberation's standard yardstick is RT60, the time to fall 60 dB — a factor of 10^(−60/20) = 10⁻³. Set level(rt60) = 10⁻³ and solve:

fb^(rt60 · fs / d) = 10⁻³   ⇒   fb = 10^(−3·d / (rt60·fs))

which is character for character the file's line in update_derived(): m_fb[v] = min(pow(10.0, −3.0·d_total/(rt60·m_sr)), k_fb_max).

The res knob maps to rt60 on a log curve before this — rt60 = k_rt60_min · (k_rt60_max/k_rt60_min)^(res_eff/100), 20 ms at res → 0⁺ up to 100 s at res 100 — because equal knob travel should mean equal ratios of decay time, which a linear map to fb spectacularly is not (all the action of a raw-feedback comb lives in the last few percent of the knob).

Now the design consequence, which is the chapter title. Because fb is re-derived from the current delay every time update_derived() runs, the ring time is the invariant: sweep a voice's frequency and fb is silently re-solved to hold rt60 constant. A raw-feedback comb has it backwards — hold fb fixed and rt60 = −3d/(fs·log₁₀ fb) is proportional to d, so low notes ring 1/f longer and high notes choke. The "resonance maps to ring time" scenario pins the calibration: inverting the log curve for rt60 = 1 s gives res ≈ 45.93, and the measured tail drops close to the ideal 30 dB over a half-second window (the test accepts 22–40 dB; it lands near 32, the excess being upper partials that the interpolator and loop lowpass damp slightly faster).

Why the delay must be fractional

At 48 kHz a 440 Hz comb needs d = 48000/440 = 109.09 samples. Round to 109 and the voice plays 48000/109 = 440.37 Hz — about +1.4 cents. Worse than the absolute error: each of the five voices quantizes differently, so the carefully-tuned beating between voices (the point of a bank) is replaced by whatever the rounding produced. The legacy abstraction had integer delays and control-rate stepping; the file header names that as the main reason it never sounded like the GRM original.

So the tap is fractional — but not linear. Reading between samples with linear interpolation is a two-tap filter H(z) = (1−η) + η·z⁻¹ where η is the fractional part, with magnitude:

|H(ω)|² = 1 − 2η(1−η)(1 − cos ω)

— a lowpass whose damping depends on η (worst at η = ½, where |H| = cos(ω/2), a null at Nyquist). Two failure modes follow. Statically, this filter sits inside the loop: its droop is applied once per round trip and compounds into the ring, so two voices with different fractional parts get different brightness decay for free. Dynamically, a sweep cycles η through 0 → 1 repeatedly, so the loop's damping ripples at the sweep rate — audible dulling and level flutter. read_hermite() is the 4-point, 3rd-order Hermite (Catmull-Rom) interpolator instead: C¹-continuous, passband flat to far higher frequency, far weaker dependence on η. Its cost is the geometry constraint noted in the code — the youngest of its four points is one ahead of the base, so d must exceed 2 strictly, hence k_min_delay_samples = 2.5 and the ceiling f_ceil = min(k_freq_ceil_hz, m_sr / k_min_delay_samples).

The feedback chain, in order

Per sample, the loop path in comb_voice::process() is:

delayed = read_hermite(d_read)  →  one-pole lowpass  →  DC blocker
        →  warp allpass  →  × fb  →  + in  →  write

The comb voice ring as a diagram: delay line, Hermite read, then the feedback chain through lowpass, DC blocker, warp allpass, and the ring-time feedback gain back to the input sum, with the d/2 pickup tap branched to the output

The lowpass is the string's brightness decay: every round trip gets a little darker, highs first, like a real string. Its coefficient is exact — m_lp_a[v] = 1 − e^(−2π·fc/m_sr) — placing the −3 dB corner at fc by construction (the one-step discretization of an RC section). The in-file comment flags the deviation: tap.comb~ used hz·2/sr, which is not even the small-argument limit of the exact map (that would be 2π·fc/fs) — its actual corner lands near fc/π, a factor-of-three tuning error on a labeled frequency knob. Faithful porting stops where the parameter lies about its units.

The DC blocker (y = norm·(x − x1) + R·y1, R = k_dc_block_r = 0.999, norm = k_dc_block_norm = (1+R)/2, ~7 Hz corner at 48 kHz) replaces the legacy tap.comb~ hard ±1 autoclip — the file's most consequential retirement. The clipper existed to stop runaway; but a clipper in a resonant loop is a distortion stage, and at high resonance — precisely where the GRM sound lives — the legacy object audibly distorted. The modern argument: cap fb below unity (k_fb_max = 0.99999), kill the loop's DC transmission (the blocker's zero at z = 1), and the linear loop contracts — no limiter needed, so res 100 rings clean.

The norm factor is this chapter's own contribution, and the story is worth a paragraph. The raw blocker (1 − z⁻¹)/(1 − R·z⁻¹) is not passive: its magnitude peaks at 2/(1+R) ≈ 1.0005 toward Nyquist. While proving the contraction claim for the first edition of this chapter, the measurement came back false in one corner: with the loop lowpass wide open the product |H_lp·H_dc| crossed unity near 450 Hz, and a voice tuned there at res 100 — where fb saturates at the cap — measurably swelled at ~+0.2 dB per second. The fix is the normalization: scaling the blocker by (1+R)/2 pins its peak gain at exactly 1 (the zero at DC is untouched), so with the allpass at unit magnitude and the one-pole lowpass ≤ 1, the loop gain is bounded by fb alone and fb < 1 now really is the airtight inequality. A kernel test pins the formerly-failing corner: 450 Hz, res 100, lp at 20 kHz, twelve seconds of ring-out, decaying window over window.

The normalization has a side effect the file also pays for: the blocker now slightly attenuates each voice's fundamental (a few parts in 10⁴ at mid frequencies, more for very low voices), which — uncompensated — would shave the top off long ring times. So update_derived divides the RT60-derived fb by dc_block_gain(ω₀), the blocker's magnitude at the voice's fundamental — the same pay-the-fundamental-back philosophy as the warp compensation below, and clamped to k_fb_max so the contraction bound survives. The second new kernel scenario pins the payoff: an impulse-excited voice at res 50 measures its RT60 within 10 % of the map's 1.41 s target.

warp: dispersion, and paying the fundamental back

The allpass is the modern GRM Comb's character control. warp sets

m_ap_c = −k_warp_coef_max · warp/100        (c ∈ [−0.85, 0])

and inserts H(z) = (c + z⁻¹)/(1 + c·z⁻¹) into the loop — unit magnitude everywhere (it cannot alter the decay), pure phase. At c = 0 it degenerates to z⁻¹, an honest one-sample delay: warp 0 is exactly the harmonic Classic comb. Its phase delay in samples is what allpass_phase_delay() computes:

τ(ω) = [ atan2(sin ω, c + cos ω) − atan2(c·sin ω, 1 + c·cos ω) ] / ω

τ(0) = (1 − c)/(1 + c)      (the DC limit the w < 1e−9 branch returns)

For negative c, τ falls monotonically with frequency — at c = −0.85, from (1.85/0.15) ≈ 12.3 samples at DC down to 1 sample at Nyquist. A partial's resonant frequency is set by its total round-trip time d_read + τ(ω), so upper partials, seeing a shorter loop, land sharp of the harmonic series — the stretched partials of a stiff piano string, exactly the physics that motivates the control.

Left there, the fundamental would sharpen too. The compensation is one line: m_d_read[v] = max(d_total − ap_tau, k_min_delay_samples), where ap_tau = allpass_phase_delay(m_ap_c, 2π·f/m_sr) is evaluated at the voice's fundamental. The main tap is shortened by precisely the phase delay the allpass adds at that frequency, so the fundamental's round trip is d_total again — pitch stays put while the overtones stretch. The max is the documented physical limit: at extreme warp × high tuning, d_total − τ falls below the interpolator's 2.5-sample floor, the loop cannot get shorter than the dispersion, and the pitch flattens — physical, and flagged in the maxref. The warp scenario pins the endpoints: warp 0 echoes at one period; warp 100 stays bounded, still resonates, and its tail correlates < 0.5 with the harmonic tail (a genuinely different spectrum, not a filter tilt).

phase: plucking the string at its midpoint

The output tap is out = y − pickup·read_linear(d_half) with d_half = d_total/2. Consider loop content at harmonic n of the voice — frequency n·f, i.e. n·fs/d. Delaying it by d/2 samples shifts its phase by

Δφ = 2π · (n·f/fs) · (d/2) = n·π

Even n: Δφ is a multiple of 2π, the delayed copy equals the original, and the subtraction (at pickup = 1) cancels it exactly. Odd n: Δφ = π, the copy is inverted, and subtraction doubles it. Pickup therefore sweeps continuously from the full series to odd-harmonics-only — plucking a string at its midpoint, where the even modes have a node. The test uses a 1 kHz voice at 48 kHz so the half tap lands on exactly 24 samples; Goertzel measures the 2f-to-f power ratio collapsing below 5% of its phase-0 value.

The parameter engine

All 22 parameters (k_num_params: gain, mix, three masters, warp, phase, freq/res/lp × 5) ride identical per-sample linear ramps. The bank keeps one count, m_ramps_active, and one flag, m_derived_dirty; process() advances only live ramps and recomputes the derived values (taps, fb, coefficients, mix gains) per sample while anything moves, once when everything has settled — the same two-tier idea as svf.h's coefficient cache, so steady state pays nothing for the smoothness.

The 16-slot preset morph is the same machinery pointed at all 22 targets at once: store_preset() snapshots the ramp targets (knob positions, not mid-ramp values); recall_preset(slot, seconds) calls ramp_to() on every parameter over n = seconds·m_sr samples. Because ramp_to() always retargets from r.current, a recall issued mid-morph — or a single slider grabbed mid-recall — is continuous by construction; no special case exists. The scenarios pin it: a 50 ms recall under a running sine produces no sample-to-sample jump above the click threshold and lands every parameter exactly on the preset; a mid-morph set_freq() reaches its own value while the other 21 keep morphing.

The engineering ledger

  • k_fb_max = 0.99999, applied after the rt60 solve. The cap makes "res 100 = longest possible resonance" a bounded statement; the rt60 math would happily request fb ≥ 1 for rt60 → ∞ (and res_master can push res_eff to 200).
  • Anti-denormal guard (|x| < 1e−15 → 0, the tap.comb~ idiom) on every recursive state — a comb ringing into silence otherwise decays into denormal territory and multiplies its CPU cost at the quietest moment.
  • Wet 1/5 normalization, a deliberate deviation. k_wet_norm = 0.2 scales the five-voice sum; the legacy abstraction wired five tap.comb~ objects straight into the output gain — a hot sum, +14 dB at full wet. The mix scenarios pin both endpoints: mix 0 is an exact passthrough, and mix 100 with all resonance off passes at unity to 1e−9.
  • Equal-power mix: m_dry_gain = cos θ · g, m_wet_gain = sin θ · g · k_wet_norm, θ = mix·π/200 — matching tap.crossfade~.
  • The pickup tap is linear-interpolated (read_linear), not Hermite — deliberate asymmetry: it is a feed-forward output tap, so its droop is applied once, not compounded per round trip; the argument that disqualified linear for d_read doesn't apply.
  • Allocation only in prepare(): one ceil(sr/k_freq_floor_hz)+8 buffer per voice (the 5 Hz floor honors the legacy 200 ms buffer). Every setter is a clamp plus a ramp retarget — safe while audio runs.

Checkpoint

A feedback comb is a string, and the file's one structural opinion is that the string's decay time — not its feedback coefficient — is the musical truth: fb = 10^(−3·d/(rt60·fs)), re-solved from the current delay so pitch sweeps preserve the ring. Hermite taps make the tuning real, the exact one-pole makes the damping knob honest, and the DC blocker retires the clipper by making high resonance a solved inequality (fine print stated) instead of a distortion stage. Warp is pure phase — dispersion paid back to the fundamental through allpass_phase_delay — and phase is pure geometry: half a loop is nπ, evens cancel, odds double. The rest is 22 ramps and one dirty flag, and the tests hold every claim.

Grains that sum to one: grm_pitchaccum.h

The user-facing chapter sold the effect on one image — +7 becomes +14 becomes +21, a staircase — and one engineering promise: "the tenth pass is as steady as the first." The image is a topology claim and the promise is an identity about a pair of window functions, and both are provable. This appendix proves them with the file's own names, then walks the pitch follower's failure mode and the ledger. The kernel scenarios in tap.pitchaccum_tilde_test.cpp drive tap::tools::pitchaccum::accum_bank directly and pin every measured claim; the sibling tap.shift~ tests pin the envelope identity to nine decimal places.

Transposition is a moving tap

A delay tap that moves changes pitch. Write the read position of a tap with delay D(n) samples behind a write head at sample n:

p(n) = n − D(n)

The output at sample n reproduces the input's phase at time p(n), so the output advances through the input at the rate:

dp/dn = 1 − dD/dn

A fixed tap (dD/dn = 0) plays at unity. A tap whose delay shrinks by (ratio − 1) samples per sample plays the buffer at ratio times real speed — up an octave means eating the delay line at one extra sample per sample. The code implements exactly this, inverted into phasor form: the tap's delay is base + window_samples · ph, and the phasor steps

m_phase += −(ratio − 1.0) / window_samples

so dD/dn = window_samples · dph/dn = −(ratio − 1), giving dp/dn = ratio. (The tt_shift provenance of that line is flagged in the code.) The transposition scenarios pin the result at the endpoints and the middle: +12 puts the energy of a 440 Hz sine at 880 Hz, −12 at 220 Hz, and 0 passes 440 untouched — each dominating its reference bin by the test's margins.

One moving tap cannot run forever — the phasor wraps, and at the wrap the tap teleports across the window: a splice. Hence the classic two-tap engine: a second tap rides the same phasor at ph_b = ph_a + 0.5 (mod 1), half a cycle apart, so one tap is always mid-window while the other is wrapping, and each is faded by envelope() so the splice happens at zero gain.

The envelope pair: an exact partition of unity

Here is envelope(ph, flank), region by region (flank ∈ (0, 0.5] is the crossfade width as a fraction of the cycle — m_flank maps xfade 1–100% onto (0.005, 0.5]):

ph ∈ [0, flank]:          sin²( π·ph / (2·flank) )      — cos²-shaped rise
ph ∈ (flank, 0.5]:        1                             — plateau
ph ∈ (0.5, 0.5+flank]:    cos²( π·(ph−0.5) / (2·flank) )— fall
ph ∈ (0.5+flank, 1):      0

The claim — stated as a comment in the file, and load-bearing — is that the taps at ph and ph + 0.5 sum to 1 exactly, at every phase and every flank width. Proof by the same four regions, writing e(·) for the envelope and using ph_b = ph_a + 0.5 mod 1:

ph_a ∈ [0, flank]:        ph_b ∈ [0.5, 0.5+flank]
    e_a + e_b = sin²(π·ph_a/2·flank) + cos²(π·ph_a/2·flank) = 1
ph_a ∈ (flank, 0.5]:      ph_b ∈ (0.5+flank, 1]
    e_a + e_b = 1 + 0 = 1
ph_a ∈ (0.5, 0.5+flank]:  ph_b wraps to ph_a − 0.5 ∈ (0, flank]
    e_a + e_b = cos²(π·(ph_a−0.5)/2·flank) + sin²(π·(ph_a−0.5)/2·flank) = 1
ph_a ∈ (0.5+flank, 1):    ph_b = ph_a − 0.5 ∈ (flank, 0.5]
    e_a + e_b = 0 + 1 = 1

Every crossfade region pairs a sin² with the cos² of the same argument; everywhere else a plateau pairs with a zero. The construction also joins each flank to its plateau with zero slope (the derivative of sin² vanishes at both ends), so there is no corner to click, and at flank = 0.5 the plateau vanishes and the pair degenerates to a complementary Hann pair.

Why demand exact? In a one-shot shifter, an envelope pair that sums to 1 ± δ is a gain ripple of δ at the grain rate — a subtle tremolo, mildly regrettable. But this object's entire identity is that the transposer sits inside a feedback loop: the ripple multiplies onto the signal on every pass, and after k trips the peaks have compounded to (1+δ)ᵏ while the troughs have decayed — a pumping that grows with exactly the feedback settings the effect is played at. That is why the original engine's window was replaced: tt_shift used a fixed 256-point padded-Welch table whose pair did not sum flat (the deviation is documented in both this file's header and tap.shift~'s). The identity is pinned numerically in the tap.shift~ wrapper tests — same envelope construction at flank = 0.5 — where DC at 0.5 pushed through moving taps at ratio 1.3 comes out equal to the input with max error below 1e−9: no grain-rate ripple, to double precision.

The accumulation topology

transposer::process() wires the loop in this order:

in ──►(+)──► delay buffer ──► two moving enveloped taps ──► y (out)
      ▲                                                     │
      └── × fb ◄── DC blocker ◄── m_fb_state (previous y) ◄─┘

The fed-back sample re-enters upstream of the taps: it is written into the buffer and then read back by the moving, windowed taps — which is to say it is delayed by delay_samples and transposed by ratio again. Every trip multiplies the frequency by another factor of ratio: +7 st becomes +14 becomes +21. Contrast the ordinary "feedback around a shifter" patch, where the feedback taps the delay output and re-enters the delay: each echo is re-delayed but shifted only once, and the staircase never climbs. The topology is the effect, and the kernel test pins its two-pass signature: a 440 Hz burst at +7 st, 300 ms delay, 70% feedback shows Goertzel energy at 659.26 Hz (one pass) in the 0.32–0.55 s window and at 987.77 Hz (two passes — the accumulation itself) in the 0.65–1.0 s window, each dominating the off-frequency reference bin.

The accumulation loop drawn against the ordinary shifter-in-feedback patch: upstream re-entry transposes every pass again; output-tap feedback shifts only once

Boundedness: the loop gain really is fb

The constant-sum envelope has a second payoff. Since e_a, e_b ≥ 0 and e_a + e_b = 1, the two-tap read is a convex combination of past loop samples — with linear taps its magnitude could never exceed the buffer's peak, and the Hermite taps can overshoot that bound only by the interpolator's small, bounded ripple. So the per-pass gain around the loop is genuinely fb — capped at k_fb_max = 0.99 — and not fb × (envelope ripple peak), which is the number that would have mattered with an uneven pair. The DC blocker in each loop kills the one component granular splicing can otherwise rectify into a ramp; its own tiny high-frequency shelf (2/(1+R) ≈ 1.0005) is absorbed a hundred times over by the 1% headroom in the cap — this kernel does not need the fine print that grm_comb.h's 0.99999 cap does. The test drives the worst case: both voices at 99% feedback, opposing transpositions, five seconds — output finite, peak < 50.

Modulation: one LFO, two seeded dice

Per sample, each voice's transposition is assembled as

trans_eff = m_ramp[p_trans1 + v].current + lfo + rnd
ratio     = 2^(trans_eff / 12)

The LFO is one global phasor (m_lfo_phase); voice 2 reads it at + modphase/360, so the two shadows breathe against each other at a settable phase — 90° by default. The random component is per-voice: tick_random() holds a target from a linear-congruential generator (seeded 1111 and 2222 in the constructor) and cosine-interpolates between held values at randrate, re-arming its phase when disabled so re-enabling starts a fresh segment rather than finishing a stale one. Deterministic seeding is a testability decision that is also a musical one — the same patch renders the same audio — and the test takes it literally: two identically configured banks render outputs with maxdiff == 0.0, bit identical. The modulation scenario pins the spectral effect from the other side: 1 st of 5 Hz LFO drains more than half the carrier bin's energy into sidebands.

The pitch follower, and the subharmonic trap

follow adapts the grain window to the input. The follower is deliberately cheap: input decimated by k_flw_decim = 8 (6 kHz at 48 kHz) into a 1024-float ring, and every k_flw_interval = 512 input samples a normalized autocorrelation over a k_flw_win = 512 window:

corr[lag] = Σ x[n]·x[n+lag] / √( Σ x[n]² · Σ x[n+lag]² )

searched over lags for 50–800 Hz. The naive readout — take the global maximum — has a classic failure mode that the code's comment names: a periodic signal correlates at every multiple of its period, so corr[2T] and corr[3T] sit at essentially the same height as corr[T], and windowing noise routinely pushes a multiple to the numerical top. The global argmax then reports a subharmonic — an octave or twelfth low — and the window snaps to twice the true period. The fix is two lines: find the global max best_r, then take the smallest lag whose correlation clears accept = max(k_flw_confidence, 0.85·best_r) — the earliest lag within 15% of the peak. Below k_flw_confidence = 0.6 nothing is accepted and period_s() reports 0: unpitched input is ignored rather than chased.

The window goal is k_flw_periods = 2 detected periods (clamped to the 5–200 ms window range), approached through a one-pole slew (m_window_eff_ms += 0.0005·(goal − eff), ≈ 40 ms time constant at 48 kHz) that relaxes back to the manual window value when follow is off or the gate says unpitched. Why two periods: at each grain wrap the tap jumps by exactly the window, so a window of k whole periods makes the splice displacement an integer number of cycles — the spliced waveform stays phase-coherent and the grain-rate artifacts land in tune with the source instead of at an arbitrary rate (the envelope cycle rate is |ratio − 1|·fs/window_samples, which at W = 2T scales with the source pitch, ≈ (ratio−1)·f₀/2). And k = 2 rather than 1 keeps the window above the 5 ms floor across most of the follower's 50–800 Hz range. The test pins both the adaptation and the trap: a 220 Hz tone converges the effective window into 6–13 ms, bracketing two periods (9.1 ms) — a subharmonic lock would demand 18.2 ms, outside the pin — and white noise leaves the window at the manual setting (70–100 ms around the 87 ms default).

The engineering ledger

  • 17 ramped parameters, same morph engine as grm_comb.h. One ramp array, m_ramps_active, m_derived_dirty recomputing derived values per sample while moving and once at settle; store_preset snapshots targets, recall_preset retargets every ramp from its current value, so mid-morph overrides are continuous with no special case. Pinned: an 80 ms recall under audio shows no sample-to-sample jump above 0.3 and lands every parameter to 1e−9; mix 0 is an exact passthrough (< 1e−9) even with 90% feedback churning inside the muted wet path.
  • The modulated ratio path lives in process(), not update_derived() — the code comments the split: LFO and random are inherently per-sample, so caching them would save nothing; the cacheable tier is delays, fb, voice gains, flank, and the equal-power mix.
  • Hermite taps with k_base_delay = 3 headroom — same 4-point interpolator and same ≥ 2-sample geometry constraint as grm_comb.h, here so that a voice delay of 0 ms is still legal under moving taps.
  • GRM's stereo-width fader is dropped, on purpose. The kernel is mono and the wrapper is single-channel by house rule (mc. wraps it); the omission is declared in the wrapper header and the maxref. Width would have been the only parameter that could not live in a mono kernel.
  • The follower is a mode, not a faderset_follow(bool) sits outside the morphable parameter set (the file says so), because interpolating a boolean analysis mode over a morph is meaningless.
  • Allocation discipline. One buffer per voice sized for the worst case ((k_max_delay_ms + k_max_window_ms) at prepare-time sr, +16), a fixed 1024-float follower ring, and a stack-local correlation array; after prepare() the audio path allocates nothing, and the analysis cost — a few hundred multiplies per input sample, amortized — is paid only when follow is enabled.

Checkpoint

A tap whose delay changes at (1 − ratio) samples per sample is a transposer — dp/dn = ratio, by one derivative. Two taps on the same phasor half a cycle apart cover each other's splices, and the cos²/sin² envelope pair sums to one exactly, region by region, at any flank width — which in a feedback loop is the difference between loop gain fb and loop gain fb-times-ripple compounding every pass. The feedback re-enters upstream of the taps, so every echo is transposed again: the staircase is a topology, and the test hears both steps. The follower reads the earliest strong autocorrelation lag, not the tallest, because the tallest is routinely a subharmonic; two detected periods keep the splices phase-coherent. Everything else — ramps, morph, seeds, caps — is the same discipline as the comb bank, and equally pinned.

Two banks and a multiplier: vocoder.h

The user-facing chapter made three flat promises about tap.vocoder~: a silent carrier is silence, gain is exactly linear, and a silent modulator decays away at the follower rate. It could afford to, because none of those is a tuning outcome — each one is a structural fact about a very small graph. This appendix draws the graph, proves the facts, and then walks the three numerical choices (band placement, filter type, follower coefficient) that make the graph sound like a vocoder.

One honesty note up front. The original tap.vocoder~ source did not survive the revival; vocoder.h is reconstructed from the reference documentation — "a basic 24-band vocoder" with q and response_interval attributes. The topology below is the classic channel vocoder that documentation describes, and the tests pin its structural behavior; there is no lost binary to bit-compare against, and this chapter never pretends otherwise.

The graph: a bilinear form in 24 subbands

A channel vocoder is subband multiplication. Split both signals with the same filter bank, measure the modulator's level per band, scale each carrier band by that level, sum:

band i:   m_i = B_i(modulator)          the modulator through bandpass i
          env_i ← follower(|m_i|)        its envelope
          c_i = B_i(carrier)             the carrier through the identical bandpass
output:   y = gain · Σᵢ c_i · env_i

That is bank::process() verbatim — the loop body computes m, rect, m_env[i], c, and accumulates c * m_env[i], and the return line applies m_gain once to the sum. Three contracts follow from the shape alone, and tests/vocoder_test.cpp pins each one:

  • Silent carrier ⇒ exactly silence. Every summand carries a factor c_i. A biquad is linear with zero state at rest, so a zero carrier gives c_i ≡ 0 for all i, and the sum is identically zero no matter what the modulator (and hence the envelopes) does. The test drives a 220 Hz modulator against a zero carrier for 8000 samples and requires peak < 10⁻¹², but the true bound is exact: 0.0 * m_env[i] is 0.0.
  • Gain is exactly linear. The multiply c_i · env_i is the only nonlinearity in the graph, and it is bilinear — linear in the carrier with the envelopes held fixed, linear in the envelopes with the carrier held fixed. m_gain sits outside all of it, a scalar on the finished sum, and nothing upstream reads it. Two banks fed identical inputs with gains 1 and 2 must differ by exactly a factor of 2, float for float; the test requires |yb − 2·ya| < 10⁻¹² across 8000 samples.
  • Silent modulator ⇒ output decays at the follower rate. With the modulator silenced, rect = 0 and each envelope obeys env ← m_env_coef · env — a geometric decay with the follower's time constant. The output is bounded by Σ|c_i|·env_i, so it decays with the envelopes even while the carrier keeps playing. The test warms the bank up, silences the modulator for one second at 48 kHz (≈ 50 time constants at the 20 ms default — a decay of e⁻⁵⁰), and requires the late output under 10⁻⁴ of the warmed level.

The fourth pinned property, determinism (two identical runs compare equal with ==), is the repo-wide claim that the kernel is pure state-machine arithmetic: no randomness, no time, no allocation in the audio path.

The vocoder graph as a diagram: two identical filter banks, per-band envelope followers, 24 multipliers, and the summed gain

The bilinear form in 24 subbands — the graph shape the proofs read off.

Where the bands sit

Twenty-four bands span 50 Hz to 12 kHz, log-spaced. band_frequency(i) computes:

f_i = k_fmin · (k_fmax / k_fmin)^(i / (k_bands − 1))     i = 0 … 23
    = 50 · 240^(i/23)

so adjacent centres sit at a constant ratio of 240^(1/23) ≈ 1.269 — about 0.344 octave, a hair over four semitones, per band. Log spacing is the only defensible choice for this machine, twice over: the ear judges musical width by ratio, not by hertz, so equal-ratio bands devote equal perceptual width to each channel; and speech puts its identity (formants, the envelope the vocoder exists to capture) in the low kilohertz while its detail (fricatives) rides above — a linear spacing would waste twenty bands above 6 kHz and cram every vowel into two. The span itself brackets speech: 50 Hz is below any voice fundamental, 12 kHz is above any formant that matters, and recalc_filters() clamps each centre at 0.45 · m_sr so the top bands stay well below Nyquist at low sample rates rather than folding.

The filter: constant peak, unconditional stability

Each band is an RBJ Audio-EQ-Cookbook bandpass, the constant 0 dB-peak variant, computed in recalc_filters():

w0 = 2π · fc / sr        alpha = sin(w0) / (2·q)        a0 = 1 + alpha

b0 =  alpha / a0         a1 = (−2·cos w0) / a0
b1 =  0
b2 = −alpha / a0         a2 = (1 − alpha) / a0

"Constant 0 dB peak" is a normalization claim: the gain at the centre frequency is exactly 1, for any Q. It is worth proving, because the whole level architecture rests on it. Evaluate the transfer function at z = e^(jw0):

H(z) = alpha·(1 − z⁻²) / [(1 + alpha) − 2·cos w0 · z⁻¹ + (1 − alpha)·z⁻²]

denominator at z = e^(jw0):
  [1 − 2·cos w0 · e^(−jw0) + e^(−2jw0)] + alpha·(1 − e^(−2jw0))
  = e^(−jw0)·(e^(jw0) − 2·cos w0 + e^(−jw0)) + alpha·(1 − e^(−2jw0))
  = e^(−jw0)·(2·cos w0 − 2·cos w0) + alpha·(1 − e^(−2jw0))
  = alpha·(1 − e^(−2jw0))                    = the numerator exactly

so H(e^(jw0)) = 1 identically. Why it matters here: env_i is supposed to measure the signal's level in band i, and each carrier band is supposed to be scaled by that measurement and nothing else. With the constant-peak variant, changing q changes bandwidth only — the on-centre gain of all 48 filters stays pinned at unity, so the q knob narrows or overlaps the bands without re-balancing the reconstructed spectrum or re-calibrating the envelope levels. The cookbook's other bandpass (constant skirt gain) has peak gain Q; with the default q = 20 that would be +26 dB per band, scaling with the knob — every q move would also be a 24-band gain move.

Stability is likewise unconditional. A biquad is stable iff its coefficients sit in the stability triangle, |a2| < 1 and |a1| < 1 + a2. Here a2 = (1 − alpha)/(1 + alpha), which lies in (−1, 1) whenever alpha > 0 — and alpha = sin(w0)/(2q) is positive for any q > 0 and any 0 < fc < Nyquist; the second condition, 2|cos w0|/(1 + alpha) < 1 + (1 − alpha)/(1 + alpha) = 2/(1 + alpha), reduces to |cos w0| < 1, true on the same range. The code enforces the preconditions rather than assuming them: q is floored at 0.001 and fc clamped to 0.45·sr, so no attribute value and no sample rate can produce an unstable band. The sections run as Direct Form I (biquad::process keeps x1, x2, y1, y2) — at these moderate Qs and double precision, the plainest form is the honest one.

The follower: one coefficient, symmetric by construction

Each band's envelope is a one-pole lowpass over the full-wave rectified band signal:

rect     = |m_i|
env_i    ← m_env_coef · env_i + (1 − m_env_coef) · rect

with the coefficient computed in recalc_envelope() from the response_interval attribute:

tau        = response_ms / 1000                      (ms → seconds)
m_env_coef = exp(−1 / (tau · sr))

That is the exact one-sample step of a continuous first-order lag with time constant τ: the discrete pole e^(−T/τ) with T = 1/sr. So the documented "analysis period" is a time constant, precisely — after response_interval milliseconds of silence an envelope has decayed to 1/e of its value, and after a step up it has covered 1 − 1/e of the distance. Note what the code does not have: separate attack and release. One coefficient serves both directions, which is what the legacy surface documents (a single response_interval) and is why the user chapter calls the knob "the vocoder's attack and release." The 10⁻⁴ floor on response_ms keeps the exponent finite; at the 20 ms default and 48 kHz, m_env_coef ≈ 0.99896.

Why time-domain, when the siblings went spectral

tap.nr~ and tap.spectra~ (next chapter) are STFT machines. The vocoder deliberately is not, for three compounding reasons:

  • Zero algorithmic latency. The spectral scaffold costs exactly one FFT frame of delay by construction; this graph's output at sample t depends only on inputs up to t. A vocoder is played live against its carrier — latency is a musical defect here in a way it is not for noise reduction.
  • It is cheap. 48 biquads (5 multiplies + 4 adds each in DF I) plus 24 follower updates and 24 multiply-accumulates — on the order of three hundred flops per sample, no transform, no windowing, no frame buffers.
  • It is faithful. The original tap.vocoder~ was a real time-domain external; the pfft~-hosted abstraction that wrapped it in some patches only added smoothing and gain around it. Rebuilding it as an FFT effect would have been reconstructing a different object. So vocoder.h follows the svf.h/ladder.h idiom — prepare(samplerate) then per-sample process() — not the configure(fftsize) scaffold of the spectral set.

The engineering ledger

  • prepare() recomputes everything. It calls recalc_filters() (24 coefficient sets, each written into both m_mod[i] and m_car[i] — the banks are identical by construction, one computation assigned twice) and recalc_envelope(). set_q re-runs only the filters, set_response_ms only the envelope coefficient, set_gain is a bare store — each setter pays for exactly what it moves, the small-scale version of svf.h's two-tier update.
  • Setters are allocation-free and audio-safe. All state is in fixed std::arrays sized by k_bands; there is no allocation anywhere in the class, so the Min wrapper can forward attribute changes from the message thread while the perform loop runs.
  • The legacy surface is honored, with one documented fix. q and response_interval keep their documented names, meanings, and defaults (20 and 20 ms). The original registered both attributes as symbol; the wrapper (tap.vocoder_tilde.cpp) registers them as number, which is what they actually are — a Q value and a millisecond time — and says so in its header. gain is a small, admitted addition for level staging, since a band-multiplied signal lands quieter than either input.
  • Both banks clear together. clear() zeroes all 48 biquad states and the envelope array — the whole graph's memory, nothing else, so a clear message can never leave a stale envelope gating a fresh carrier.
  • What is deliberately absent: per-band gain trims, separate attack/release, a noise-driven "unvoiced" band — all classic vocoder extensions, all outside the documented surface being reconstructed. The reference page promised a basic 24-band vocoder; the file implements exactly that and stops.

Checkpoint

The vocoder is a bilinear form: two identical 24-band banks and one multiply per band. Everything the tests pin — silence in, silence out; exact gain linearity; follower-rate release — is a consequence of that shape, not of tuning. The numerics are three choices: log spacing (equal ratio per band, matched to hearing and to speech), the constant-peak RBJ bandpass (band level measures the signal, not the Q, and stability is a theorem with the clamps in place), and the exact one-pole coefficient e^(−1/(τ·sr)) (the documented period is an honest time constant, symmetric in both directions). Time-domain because latency, cost, and history all point the same way.

One STFT, three effects: fft.h, stft.h, nr.h, spectra.h

The user-facing chapters for tap.nr~ and tap.spectra~ both lean on the same claim: the machinery is transparent — set the effect to do nothing and the output is the input, exactly, one FFT frame late. All the trust in these objects lives in that claim, and it is not free: it has to be engineered into the window, the overlap, and one normalization constant. This appendix builds the stack bottom-up — the FFT, the scaffold, then the two small effects on top — and proves the transparency claim rather than asserting it.

fft.h: the transform, owned outright

The kernel repo's law is zero dependencies — plain C++17, standard library only. So the FFT is in-house: an in-place iterative radix-2 Cooley–Tukey in fft::transform(re, im, inverse), about forty lines. It is also one copy by design: the identical routine previously lived, byte for byte, inside conv_engine (tap.convolve~), tap.nr~, and tap.spectra~, and was consolidated at the kernel split so it is maintained and tested once — tests/fft_test.cpp is that single test point. The two halves:

  • Bit-reversal permutation. An iterative FFT consumes its input in bit-reversed index order; the first loop swaps each element i with its bit-reversed partner j, maintaining j incrementally (the carry-ripple idiom) rather than reversing bits per index; the i < j guard swaps each pair once.
  • Butterfly stages. For each length len = 2, 4, … N, combine pairs of half-blocks with twiddle factors e^(∓2πik/len). The twiddle is advanced by a complex-multiply recurrence (cwr, cwi rotated by (wr, wi)) — one cos/sin per stage instead of per butterfly. A recurrence accumulates rounding, but in double precision over these sizes it is far inside the pinned tolerance.

The scaling convention is asymmetric and load-bearing: forward is unscaled, inverse divides by N, so forward-then-inverse is the identity. Every claim is pinned in fft_test.cpp: the forward transform matches a naive O(N²) DFT to 10⁻⁹ for N ∈ {2, 4, 8, 16, 64, 256}; the round trip reconstructs random complex input to 10⁻⁹ at N = 128; a unit impulse transforms to an exactly flat unit spectrum; and a real cosine at bin 3 of 32 lands N/2 on bins 3 and 29 — fixing the sign convention (forward kernel e^(−i…)) and the conjugate-bin layout the effects below depend on.

stft.h: the scaffold, and the COLA proof

The STFT scaffold as a diagram: ring buffers and windows bracketing the FFT, the pluggable op, and the COLA-normalized overlap-add

One pump, two effects: nr and spectra are this pipeline with different middles.

stft is the overlap-add machinery shared verbatim by both effects: Hann window, fixed 4× overlap (m_hop = m_fftsize / m_overlap), a circular input buffer, a circular output accumulator, and a per-sample pump. process() takes the effect as a callable — op(re, im, N) mutates the N-point spectrum in place between the forward and inverse transforms; the only difference between tap.nr~ and tap.spectra~ is that lambda. The window, built in configure():

m_window[k] = 0.5 − 0.5·cos(2π·k / m_fftsize)        k = 0 … N−1

— the periodic Hann (denominator N, not N−1), which is what makes the overlap sums below exactly constant rather than rippling. The window is applied twice per frame: once at analysis (m_re[k] = inbuf·window[k]) and once at synthesis (outbuf += m_re[k]·m_window[k]·m_norm). With an identity op, the inverse transform returns the windowed frame exactly (the FFT round trip is the identity), so each input sample x is delivered to the output through every frame that covers it, weighted by w² each time:

y[t] ∝ x[t−N] · Σₘ w²(n − mH)          H = N/4, four frames cover each n

Perfect reconstruction therefore requires the shifted window-squared sum to be constant — the COLA (constant overlap-add) condition for double-windowing. For the periodic Hann at 4× overlap it is, and the constant has a closed form. Expand w²:

w²(θ) = (0.5 − 0.5·cos θ)² = 0.375 − 0.5·cos θ + 0.125·cos 2θ     θ = 2πn/N

A hop of N/4 advances θ by π/2. Over four hops, the cos θ terms are four quarter-turns of a phasor — they sum to zero; the cos 2θ terms advance by π per hop and cancel in adjacent pairs. What survives is the constant:

Σₘ w²(n − mH) = 4 × 0.375 = 3/2         for every n

The code does not hard-code 3/2. configure() overlap-adds overlap copies of m_window[k]² around a circular buffer and reads the value at cola[m_fftsize/2], setting m_norm = 1/c — for Hann at 4×, m_norm = 2/3 (verified numerically: the computed sum is 1.5 to within 10⁻¹⁵ at every index, so reading the midpoint is safe). Computing it keeps the scaffold correct for any window/overlap it might grow.

Latency is exactly N, and here is the accounting. The pump writes in[i] into m_inbuf[m_pos], reads the output from m_outbuf[m_pos], zeroes that slot, advances, and fires a frame every m_hop samples. When a frame fires, its index k holds input sample x[t₀ − (N−1) + k] (t₀ the newest sample, at k = N−1), and synthesis writes index k into m_outbuf[(m_pos + k) % N], which the pump reads k+1 samples later. Output time minus input time:

(t₀ + 1 + k) − (t₀ − N + 1 + k) = N          for every k, every frame

A frame cannot be transformed until it has filled — that is the whole cost, and why latency() simply returns m_fftsize. Both test suites pin the full contract at once: with a do-nothing effect (nr at threshold 0, spectra at remap 1), out[t] == in[t − N] to within 10⁻⁹ on broadband noise for all t ≥ 2N (the run-in covers frames that still window in zeros). This is the "transparent machinery" sentence in both user chapters, with its provenance attached: FFT round trip (pinned) × COLA constant (derived) × exact-N pipeline (derived).

nr.h: the gate, precisely

The spectral op is gate(), and its knee is short enough to quote in full as math. Per bin k:

mag  = √(re[k]² + im[k]²) · (2/N)
gain = 1                                if thr ≤ 0 or mag ≥ thr
gain = (mag / thr)^slope                if mag < thr   (slope ≤ 0 → 1)
re[k] *= gain;  im[k] *= gain

The 2/N puts mag on a sinusoid-amplitude scale (a real tone of amplitude A puts A·N/2 in each of its two conjugate bins; ×2/N recovers A). One honest calibration note: the frame is Hann-windowed before the FFT, and the Hann's coherent gain is 1/2 — a bin-centred sine of amplitude A actually measures mag = A/2 (verified numerically: A = 0.8 reads 0.400), with leakage in the adjacent bins. threshold is a linear amplitude on the windowed scale; a full-scale sine sits near 0.5, not 1.0.

The knee is a downward expander per bin. Take logs of the gain law below threshold:

L_out − L_thr = (1 + slope) · (L_in − L_thr)         in dB

Every dB below the threshold becomes (1 + slope) dB below it: slope 0 is unity (bypass by another name — the code special-cases it), the default slope 2 is a 1:3 expander, and slope → ∞ approaches a hard gate. Both re and im are scaled by the same real gain, so phase is untouched — the gate reshapes magnitude only.

Two structural notes. First, the loop runs over all N bins, mirror half included, with no symmetry bookkeeping — and needs none: the input frame is real, so its spectrum is Hermitian, magnitudes are symmetric (mag[N−k] = mag[k]), conjugate pairs get the same real gain, and Hermitian symmetry survives the op — the inverse stays real for free. (Hold that thought; spectra is not so lucky.) Second, the per-frame independence of the gain decision is exactly where musical noise comes from: a bin whose magnitude hovers near thr flips between pass and heavy attenuation frame by frame, and each isolated pass is one Hann-windowed near-sinusoid burst, milliseconds long — a chirp. Scattered over time and frequency, chirps sound like water. That is not a bug in the code; it is the knee's steepness meeting the frame rate, which is why the user chapter's cure is a gentler slope, not a different implementation.

tests/nr_test.cpp pins the three defining behaviors: gate open (threshold 0) reconstructs noise to 10⁻⁹ delayed one frame; a quiet bin-centred tone (amplitude 0.05 against threshold 0.5, slope 4) leaves a steady-state tail under 5 % of the input RMS; a loud tone (0.8 against 0.01) passes with RMS within 2 % of the input.

spectra.h: the remap, and why the mirror is not optional

The op builds a new spectrum over the lower half:

src      = lround(k · m_remap)                k = 0 … N/2
m_ore[k] = re[src], m_oim[k] = im[src]        if 0 ≤ src ≤ N/2, else 0

then forces DC and Nyquist real (m_oim[0] = m_oim[half] = 0), mirrors — m_ore[N−k] = m_ore[k], m_oim[N−k] = −m_oim[k] for 0 < k < half — and copies the scratch back over re/im.

The mirror is provable necessity, not tidiness. A real signal's DFT satisfies X[N−k] = conj(X[k]), and only Hermitian spectra invert to real signals. The remap fills the lower half by an arbitrary rule and touches nothing above Nyquist — the upper half still holds the input's bins, so the assembled spectrum is not Hermitian for any remap ≠ 1, and its inverse transform is genuinely complex. And the scaffold's synthesis reads m_re only — the imaginary part of the inverse is discarded. Keeping Re(IDFT(Y)) is algebraically inverse-transforming the Hermitian average ½(Y[k] + conj(Y[N−k])): without the mirror, the delivered effect would be an uncontrolled blend of the remapped lower half and the untouched upper half, half the intended signal shunted silently into the discarded imaginary part. The mirror makes the spectrum Hermitian by construction, so the inverse is exactly real and "keep the real part" loses nothing. DC and Nyquist are their own mirror images (k = N−k), so conjugate symmetry forces them real — hence the two explicit zeroes.

(The remap cannot run in place — output bin k may read a bin already overwritten — hence the m_ore/m_oim scratch, allocated in configure().) And the energy honesty: a remap is not a permutation, so Parseval is deliberately broken. For remap < 1, lround(k · remap) is non-strictly increasing — several output bins read the same input bin, duplicating its energy. For remap > 1 it strides — input bins are skipped, and every output bin above N/(2·remap) reads beyond Nyquist and is zeroed, discarding the input's top octaves. Neither direction conserves energy, and neither is meant to: the reference page has called this an "ultra-non-linear effect" since 2002; the kernel implements the rule, not a transform.

tests/spectra_test.cpp pins the two anchors: remap 1 reconstructs noise to 10⁻⁹ delayed one frame (the identity copies the lower half of an already-Hermitian spectrum, and the mirror rebuilds the upper half it started with); remap 2 moves a tone at input bin 16 to output bin 8 — lround(8 · 2) = 16 — verified by FFT-ing a frame-aligned slice of the steady-state output and requiring the peak at bin 8.

The engineering ledger

  • The effect is a template parameter, not a base class. stft::process takes SpectralOp&& and calls it once per hop; each effect passes a capturing lambda. No virtual dispatch in the audio path, full inlining.
  • Why the vocoder is not the third client. tap.vocoder~ is time-domain on purpose — zero latency, no frame, prepare(sr) instead of configure(fftsize) — see its own chapter. The spectral set accepts latency as a cost model; the vocoder's whole point is not paying it.
  • Allocation at configure() only. Window, in/out rings, FFT scratch, and (for spectra) the remap scratch are all sized there; process() is allocation-free. reset() flushes the running buffers without reallocating or touching the window — commented in the code as safe from a message handler, which is exactly how the wrappers use it.
  • fftsize is the one shared dial, and the scaffold makes its price explicit: resolution (bin spacing sr/N), smearing (a per-bin decision spreads over a whole frame), and latency (latency() returns N so the wrapper can report a true number to the host).
  • One FFT, tested once. The round-trip and DFT-reference pins in fft_test.cpp are what let this chapter treat "forward then inverse is the identity" as a premise everywhere above.

Checkpoint

The stack is three honest layers. The FFT is forty owned lines with an asymmetric scaling convention, pinned against a naive DFT. The scaffold windows twice, so reconstruction needs the shifted w² sum to be constant — for periodic Hann at 4× overlap it is exactly 3/2, measured rather than assumed, and the pipeline delays every sample by exactly N. On top, each effect is one spectral op: nr a per-bin 1:(1+slope) downward expander whose real gain preserves Hermitian symmetry for free; spectra a bin-index rule violent enough that reality — a real output — must be restored by explicit mirror. Transparency at the neutral setting is the theorem the whole stack exists to satisfy; both suites pin it at 10⁻⁹.

Seventeen, not four: diode_ladder.h

The transistor-ladder appendix derived why the Moog loop oscillates at k = 4. The 303's filter looks like the same idea — four capacitors, one feedback path — and behaves like a different species, because the diode ladder deletes the one luxury the Moog circuit has: buffering. Every diode pair both charges the next capacitor and loads the previous one. This appendix derives what that coupling does to the poles, why the oscillation threshold lands at exactly 17, why the shipping filter still refuses to self-oscillate at stock settings, and how the coupled nonlinear system is solved in closed form every sample.

Everything here is verified against Tim Stinchcombe's published TB-303 circuit analysis and the executed tb303.ipynb notebook, which matches the kernel's linearized response to his transfer function to 0.028 dB.

The chain: a diffusion line, not a cascade

With the diode conduction curve linearized (identity for now), the four node voltages obey a coupled chain:

v1' = ω·(S(u − v1) − S(v1 − v2))
v2' = ω·(S(v1 − v2) − S(v2 − v3))
v3' = ω·(S(v2 − v3) − S(v3 − v4))
v4' = 2ω·S(v3 − v4)          [the top capacitor is halved on the schematic]

Each middle equation has two terms — charge in from the left, charge stolen by the right. That is the loading, and it is the whole story: this is a discrete diffusion line, not four independent one-poles.

The diode ladder as four coupled capacitor nodes with charge flowing in from the left and stolen back by the right at every junction, a halved top capacitor, and the feedback path from the top node through the 150 Hz high-pass and the resonance gain back to the input sum

Every edge is a tanh; every middle node leaks both ways. The bidirectional arrows are what a buffered cascade doesn't have — and why the poles spread. Its normalized transfer function works out to exactly Stinchcombe's measured TB-303 response,

H(s) = 1 / (s⁴ + 6.727·s³ + 14.142·s² + 9.514·s + 1)

and the coefficients are not arbitrary — they are 4·2^(3/4), 10·√2, 8·2^(1/4): the equal-component chain with the top cap halved, which is also why Stinchcombe finds that changing that one cap shifts cutoff by 2^0.25. The poles are all real and spread ~25:1 (−0.13, −1.04, −2.33, −3.24 normalized). Compare the Moog ladder: four coincident poles. Consequences you can hear:

  • Asymptotically the slope is 24 dB/oct, but only ~14 dB falls in the first octave above cutoff — the honest version of the panel's "18 dB" claim.
  • At resonance 0 the −3 dB point sits ~3.2 octaves below the resonance frequency. The kernel's frequency parameter names the resonance peak, and the wide skirt below it is the real filter, not a tuning bug.

Seventeen: the closed loop's threshold

Feedback enters as u = drive·x − k·hp(v4). Ignore the high-pass for a moment and run Routh–Hurwitz on the closed loop: the stability boundary lands at exactly k = 17, with the marginal oscillation at √2× the stage rate. (Open303 normalizes its feedback by the same 1/17.) So resonance maps k = 17·resonance, putting 1.0 at the ideal chain's threshold — and the prewarp is chosen so that the √2 factor lands the oscillation on the labeled frequency:

g = tan(π·fc / fs_os) / √2       per stage (2g on the top stage)

Why a stock 303 never quite sings

Now put the high-pass back. The hardware's resonance feedback runs through a ~150 Hz one-pole high-pass (Open303's calibrated value, the fbhp default), and its phase lead pushes the would-be oscillation frequency up — to where the ladder attenuates more. Measured on the shipping kernel: the closed loop needs k ≈ 17.5 even at 8 kHz, ≈ 19 at 2 kHz, ≈ 25 at 500 Hz. The knob stops at 17. The emergent result — not programmed, derived — is the famous trait: a stock TB-303 never quite self-oscillates, and neither does this filter until you take the documented bend (resonance runs to 1.5, i.e. k = 25.5; past ~1.1 it sings at high cutoffs, slightly sharp of fc for the same phase-lead reason).

The high-pass buys two more behaviors for free:

  • Resonance thins as cutoff falls — low notes squelch instead of ringing, which is why a 303 keeps its bass at high resonance.
  • Closed-loop DC gain is exactly 1 regardless of resonance. The transistor ladder needed a comp parameter to buy its passband back; the 303's own circuit is the compensation, so this kernel has none.

Set fbhp 0 and the ideal analysis becomes exact: threshold at 1.0, oscillation at fc, drifting flat by ~0.7× the resonance excess past threshold — an amplitude effect (the growing swing saturates the edges unevenly), identical at every cutoff, and pinned by test.

The nonlinearity lives on the edges

In the circuit the coupling elements saturate — there are no buffer amps between stages to saturate instead. So the kernel puts tanh on every S(·) above: four saturators, one per diode-pair edge, slope 1 at the origin so small signals see exactly the linearized Stinchcombe response. There is no asym parameter here, deliberately: the diode pairs are complementary, so the transistor ladder's operating-point-mismatch story does not apply.

Solving the coupled system in closed form

The ZDF discretization (trapezoidal, as everywhere in the house) turns each sample into a system: five unknowns (v1..v4 and the loop input u) that all depend on each other through the couplings and the feedback. The kernel linearizes each edge with a secant gain γ = tanh(e)/e at an operating point, and then — this is the part worth reading in the code — eliminates the linear system bottom-up to closed form: v4 in terms of v3, then v3 = p30 + p32·v2, v2 = q20 + q21·v1, back-substituted until one division yields v1 and everything else follows. No matrix, no pivots, and unconditionally stable: every divisor is ≥ 1 for g > 0, γ ∈ (0, 1]. The feedback high-pass's state enters the same solve (its instantaneous gain 1 − G multiplies k), so the loop is closed exactly, high-pass included.

The two solvers differ only in how the secant gains chase the operating point:

  • solver_fast (default): solve at the previous sample's gains, refresh the gains at that solution, solve once more, commit.
  • solver_exact: repeat the refresh-and-solve until the node voltages move by less than 1e-12 (capped at 32 iterations).

Measured across a settings matrix out to resonance 1.4 and +24 dB drive — beyond hardware reach — the worst-case difference is −44.9 dBr, at 1.6–3.3× the CPU. The fast path's one correction is almost always enough because tanh is smooth and the operating point moves slowly at audio rate; the exact path exists so that claim never has to be taken on faith.

After the solve, the states advance trapezoidally using the true diode currents (tanh at the solved voltages, not the secant approximations) — the same "linearize to solve, commit with the real nonlinearity" pattern as ladder.h's one-pass commit.

Oversampling and the rest of the housekeeping

tanh generates harmonics; harmonics alias. The kernel runs 1×/2×/4× (default 2×) with zero-stuffing and matched 4th-order Butterworth anti-image/anti-alias cascades — the ladder.h pattern, self-contained here per the house rule against shared lookup tables. Every parameter rides a per-sample linear ramp; 16 preset slots morph through the same ramps; the right-inlet path recomputes the coefficient per sample for signal-rate cutoff. All state clears to zero, and all-zero state is a fixed point — a self-oscillating patch needs a ping, exactly like the transistor ladder.

The engineering ledger

  • Coupled solve vs. buffered shortcut. A "diode ladder" built from four buffered one-poles with new constants would miss the pole spread — the defining character. The 2×-larger algebra of the coupled solve is the price of the topology, paid once in closed form.
  • Secant linearization vs. Newton. Newton needs the derivative of five tanh terms through the elimination; secant gains reuse the same elimination unchanged and converge fast enough that solver_exact rarely iterates more than a few times. Same accuracy target, simpler code.
  • WDF: the documented no-go. A wave-digital rebuild of the same network was evaluated and declined (author-approved, 2026-07-18): solver_exact already converges the circuit's nonlinear equations, and a WDF would re-solve the same network differing only through the Shockley-vs-tanh diode curve, with no measured reference showing an audible delta to chase. The evidence lives in the notebook's solver A/B matrix.
  • No asym, no comp. Both absences are circuit facts, not omissions: complementary diode pairs, and a high-pass that is its own passband compensation.

Checkpoint

A diffusion chain whose transfer function matches the published analysis to 0.028 dB, with the oscillation threshold derived at k = 17 and then — because the feedback high-pass is modeled rather than idealized away — never reached at stock settings, exactly like the hardware. The nonlinearity sits on the coupling edges where the circuit puts it; the coupled ZDF system is eliminated to closed form and solved once or iterated to convergence, with −44.9 dBr between the two answers at settings the hardware can't reach. The character is the coupling, and the coupling is solved, not approximated away.

The couplings are the instrument: tb303_voice.h

The field-guide chapter argued that the 303 is unmistakable because its blocks are coupled — accent reaches the filter and the amplifier through shared circuitry with memory, slide is gate behavior, the envelopes have fixed interrelations. This appendix walks the per-sample code that implements those couplings: the measured envmod law, the C13 accent-sweep capacitor, the slide one-pole, the square shaper, and the phase-2 VCA. The filter itself is the previous appendix; this file composes it.

Sources, and the division of labor between them: Open303 (Robin Schmidt) supplies the measured constants — knob travels, envelope times, the envmod mapping, the square-shaper curve; the Devil Fish documentation (Robin Whittle) supplies the circuit behavior of the envelope/accent path, including the one place this kernel deliberately diverges from Open303. Every constant in the header carries its source.

One sample, in order

process() reads top to bottom as the signal path: pitch (with slide) → envelopes → the C13 update → the cutoff sum → oscillator and shaper → coupling high-pass → diode ladder → VCA → output coupling. Each stanza below is one of those steps.

Signal-flow diagram of the 303 voice with the couplings highlighted: the accent bus fanning to the envelope clock, the C13 charge path, and the VCA; the C13 capacitor feeding the cutoff sum

The file, as a schematic. Grey is what every clone has; red is what accent touches; amber is the cutoff CV that C13 leans on.

Slide: one coefficient, no special case

m_pitch += (m_pitch_target − m_pitch) · m_slide_coef

That is the entire slide implementation: a true RC lag (τ = the slide parameter, stock 60 ms — Open303's slideTime) on the pitch target. The gate logic makes it behave like the hardware: note_on with the gate low snaps m_pitch to the target before retriggering (a fresh note starts in tune); set_pitch with the gate held moves only the target, so the lag glides and neither envelope retriggers. Legato is slide — which is why the sequencer's gate-hold trick (see step_seq.h) needs no slide wire of its own.

Two envelopes, both RC discharges

The Main Envelope Generator and the VCA envelope are the same primitive — one-pole rise, exponential decay — with different constants and one coupling each:

  • MEG: 3 ms attack; decay = the decay knob (200 ms–2 s)… unless the note is accented, in which case the hardware bypasses the pot and runs at ~200 ms (accdecay, a bend, adjusts this clock). Faster and hotter is half of what "accent" means.
  • VCA env: fixed — ~3 ms attack (the Devil Fish "Soft Attack" bend widens it to 0.3–30 ms), a measured 1.23 s decay with no sustain, chopped by a 2 ms release at gate-off (Open303 measures ~1 ms; 2 is click-free). No knobs on the hardware, so no knobs here.

C13: the wow, as three lines of code

The accent sweep circuit is a diode feeding a capacitor through the resonance pot. The kernel's model is exactly that sentence:

drive = accent_knob · note_accent · meg
if (drive > c13)  c13 += (drive − c13) · charge     // diode conducts: τ = 47 ms  (47k·1µF)
c13 −= c13 · drain                                   // always draining: τ ≈ 150 ms

The diode gating (if drive > c13) is the memory: between closely spaced accents the drain doesn't finish, so the next accent starts from residual charge and peaks higher — the build-up. The notebook measures the cutoff peak growing ×1.94 across a run of accents and returning within ×0.998 once they stop. The cutoff contribution combines the capacitor voltage with a direct MEG term reduced by it (Devil Fish: "~100/147 of the MEG minus the capacitor voltage" — what rounds the first accent's curve):

res_mix = 0.3 + 0.7·min(resonance, 1)     // the pot is ganged with resonance
acc_oct = 2.0 · res_mix · (0.4·max(drive − c13, 0) + c13)

Two things to note honestly. The RC time constants are component-derived; the sweep span (2 octaves) and the 0.4 direct weight are informed approximations, flagged as such in the header. And this is the kernel's one deliberate divergence from Open303, which models its accent path as a plain 15 ms leaky integrator with no across-notes memory. The A/B was done for real — Open303 built and rendered side by side — and the Devil Fish circuit description won because the memory is documented hardware behavior. The divergence is recorded in the header, not buried.

The cutoff sum: a measured law, not a mixer

envmod is not "envelope amount into a summing node." Open303 measured the hardware's actual mapping (calculateEnvModScalerAndOffset), and the kernel uses those regression lines verbatim. With c the knob's log-position between the measured travel endpoints (302…2394 Hz):

scaler = (1−c)·(3.774·e + 0.737) + c·(4.195·e + 0.864)
offset = 0.0483·c + 0.2944
fc_eff = cutoff · 2^( scaler·(meg − offset) + acc_oct )

The offset term is the hardware's "gimmick": turning envmod up also injects a counteracting DC shift, so the sweep's resting point moves down as its depth grows — roughly 2/3 of the sweep lands above the knob position and 1/3 below. That interaction is why the knobs feel like a 303 rather than like a synth with the same ranges. Note acc_oct adds outside the envmod scaling: in the circuit the accent sweep injects directly into the cutoff sum, so accents quack even with envmod at zero.

The square that isn't

The 303's square is its saw pushed through a transistor shaper, and Open303 measured the resulting curve. The kernel takes the polyBLEP saw (vco.h's machinery), makes a half-cycle-shifted copy, and applies the measured shaper:

square = −tanh( 10^(36.9/20) · shifted + 4.37 )

That ~70× gain and the 4.37 bias produce the rounded, notched pulse whose spectrum audibly differs from an ideal 50 % square. The waveform parameter is a ramped blend between saw and shaped square, so switching glides click-free.

The couplings at the edges: two high-passes

Two one-pole high-passes bracket the filter — 44.5 Hz before it, 24.2 Hz after (both Open303-calibrated coupling corners). The post-filter one earns its keep twice: it is the output coupling, and in vca warm mode it absorbs the saturator's signal-dependent DC, which is exactly what the hardware's coupling capacitor does.

The phase-2 VCA: distortion that tracks the envelope

vca clean is a multiply — bit-identical to phase 1. vca warm models the one-transistor class-A stage as a slope-normalized biased saturator applied after the envelope gain and before the output coupling (the hardware order):

S(v) = ( tanh(d·v + b) − tanh(b) ) / ( d·sech²(b) ),   d = 2.0, b = 0.3

Unity slope at zero means quiet notes pass essentially clean; the bias means hot signals pick up even harmonics and compression. Because the envelope sits inside v, the distortion tracks it: measured 5.4 % difference-signal on quiet notes, 11.5 % on full accents, ~11 % second harmonic on a full-scale sine with ~−4 dB of compression. d and b are probe-calibrated informed constants — the header flags schematic-derived values as an audition-time refinement, which is the honest state of things.

Per-unit spread: seed/tolerance

The house vco.h convention, applied to a whole voice: tuning trim, cutoff scale, envelope times, slide and C13 RCs each take a deterministic per-seed offset scaled by tolerance, and the oscillator receives the seed plus a proportional imperfect amount. tolerance 0 is the nominal schematic, bit-identical to an unseeded voice (pinned by test); an mc. stack with different seeds drifts apart the way a wall of real units does.

The engineering ledger

  • One object, not a modular kit. The C13 path touches the MEG, the resonance knob, and the cutoff sum; accent touches the MEG clock, the VCA gain, and the sweep. Decomposed into osc + filter + env externals, every one of those wires would be the user's problem and most patches would omit them. The couplings live between the blocks, so the object boundary goes around them.
  • Measured constants over derived ones, where measurements exist. Open303's envmod law and shaper curve are adopted verbatim rather than re-derived from the schematic — they were measured against hardware, and re-derivation would add error, not rigor. Where Open303 simplifies (the accent memory), the circuit description wins instead. Each choice is sourced at the constant.
  • The wow's parameters are honest approximations. Sweep span and the direct weight await a hardware-calibration pass; the shape (diode gating, two RCs, resonance ganging) is circuit-derived and pinned by the ×1.94 measurement. Flagged, isolated, waiting — the autowah pattern.
  • process_at() per sample. Pitch (note + tuning + slide) can change every sample, so the oscillator is driven at signal rate rather than through a control-rate frequency parameter. The slide RC would be audibly steppy any other way.

Checkpoint

A voice whose per-sample loop is the schematic's block diagram: slide as one RC coefficient plus gate logic, envelopes as discharge curves with the hardware's fixed interrelations, accent as a hotter-and-faster MEG plus a diode-gated capacitor whose leftover charge is the wow, a cutoff law measured off real hardware complete with its gimmick, a square that is a shaped saw because that's what a 303's square is, and a VCA whose warmth tracks the envelope because the envelope sits inside the saturator. Every constant carries its source, and the one divergence from the reference implementation is documented with its reason.

One network, eight voices: the tr808_* headers

Roland built an entire drum machine out of about four circuit ideas, so the kernel does too: a bridged-T resonator class, a six-oscillator metal bank, a noise/VCA toolkit, and eight thin per-voice headers that compose them. This appendix covers the shared blocks' math — the bridged-T's trapezoidal solve and why the bass drum needs it solved that way, the metal bank's tolerance model — and the per-voice compositions, ending with the calibration pass that re-fit the family's envelopes against a real unit.

Provenance: the Werner–Abel–Smith papers (the DAFx-14 bass-drum analysis and the cymbal/cowbell companions) and the TR-808 Service Notes, read component by component. Every constant in these headers carries a schematic designator or a paper section; the calibration numbers live in tr808_calibration.ipynb.

bridged_t.h: the universal voice circuit

The bridged-T resonator drawn as a circuit: op-amp, capacitive arms, bridge and leg resistors, the exposed Vcomm node, and the kick's per-sample leg modulation

The network every voice reuses, with the kick's circuit-bending drawn in red.

An op-amp with a bridged-T network in its feedback path — capacitive arms C_a, C_b, a resistive bridge, a resistive leg to ground — rings when kicked, as a decaying pseudo-sinusoid at

fc = 1 / ( 2π · sqrt(R_leg_eff · R_bridge · C_a · C_b) )

where R_leg_eff is the leg in parallel with every resistive injection into the center node. Roland used this network in every voice: as the resonator of the kick, snare, toms/congas, rimshot, and claves, and as the band-pass of the clap, cowbell, cymbal, and hats. One class, one family.

Two implementation decisions matter:

  • The topology is reproduced, not summarized. With injections grounded, the class's transfer function matches the DAFx-14 paper's printed Eqn. (5) coefficient by coefficient (β₂ = α₂ = R_eff·R167·C41·C42, and so on); the injected paths match their Hbt2/Hbt3, interchanged by injection resistor; and the center node the paper calls Vcomm is exposed, because the bass drum's pitch-sigh nonlinearity reads it. The whole thing was re-derived by nodal analysis and pinned by unit test — the paper is trusted, then verified.
  • Trapezoidal on the states, not bilinear on the coefficients. The discretization uses capacitor companion models — a 2×2 linear solve per sample — which is algebraically the bilinear transform the paper uses, but solved on the network states directly. The reason is the bass drum: its leg resistance is modulated per sample (the attack shift shorts a resistor through Q43; the pitch sigh shrinks the effective leg through a fitted nonlinearity). With a coefficient-form biquad that would mean a full redesign every sample; with the companion-model solve, a time-varying resistor is just a changed matrix entry. Same ZDF family as the house svf.h.

The kick, since it exercises everything

tr808_kick.h composes the resonator with the paper's full block diagram: pulse shaper → retrigger network → bridged-T with a feedback buffer closing a regeneration loop → tone → level. The three signature behaviors are all emergent from the modeled schematic: for ~6 ms the envelope saturates Q43 and the ring sits near ~129 Hz (the attack punch); as the envelope collapses, C39/R161/D52 kick the center node again (the retrigger, so the note doesn't step down); and leakage lifts Q43's base when the center node swings below a diode drop — the paper's fitted memoryless nonlinearity (α = 14.315, V₀ = −0.556, m = 1.4765e-5) converts Vcomm to a collector current that shrinks the leg, so big early swings ring sharp and relax down as the note decays. That is the sigh, and it is a different mechanism from the attack jump — the paper's central untangling, preserved here. One erratum survives in the header: the paper's Eqn. (9) as printed is garbled, so the leg formula was re-derived from KCL at Q43's collector and matches their stated limits.

Accent is the trigger voltage — 4–14 V on the bus, mapped from the 0..1 edge amplitude — exciting the network harder, not scaling the output. And filter states persist across triggers, so rolls interfere with the ringing tail: no machine-gun effect, by construction rather than by crossfade.

swing_vca.h: the small shared parts

The 808 shapes its percussive gains with one-transistor "swing type" VCAs driven by RC discharges, not ADSRs. The header holds the three primitives the noise voices share: decay_env (one-pole rise to a level, exponential decay — retriggering re-aims the rise, no reset click), the linear swing_vca gain (the hardware's "many high harmonics" are a flagged refinement), and white_noise — a seeded xorshift64*, because the 808 has exactly one noise generator feeding the snare's snappy, the clap, the maracas, and the toms' noise layer, and because determinism-per-seed is a house invariant: renders reproduce, tests pin, mc. instances decorrelate.

metal_bank.h: six squares and a spread

The metallic voices all draw on one bank of six Schmitt-trigger relaxation oscillators: nominal 205.3, 369.6, 304.4, 522.7 Hz plus the two trimmer-tuned at 800 and 540 (the pair the cowbell taps), duty 47.98 % per the paper's HD14584 analysis. Three modeling calls:

  • Naive squares are faithful. The fundamentals sit below 1.2 kHz and the hash above them is immediately band-passed; the residual aliasing folds into the same inharmonic wash the circuit itself produces. PolyBLEP would be cost without benefit — a rare sentence in this repo, so it's documented.
  • Tolerance is part of the instrument. The RC parts put any given unit's oscillators up to ~20 % off nominal — the paper's measurement, and the reason no two 808s' cymbals sound alike. tolerance scales a deterministic per-seed spread of exactly that width. This is not "analog warmth" seasoning; it is a measured production statistic.
  • The two band-pass voicings (~3440 and ~7100 Hz, Q fit to the paper's published skirts) and the Q19 attack smoother (τ = 102.44 µs less a 0.7258 V base-emitter drop, their least-squares fit) live here too, because cymbal, hats, and cowbell all share them.

The voices, as compositions

Each tr808_*.h is a thin arrangement of the blocks above, with its own schematic constants:

  • Snare: two bridged-Ts at the late-revision ~173/336 Hz (the design change is documented in the header), a trigger divider, and the snappy path — decay_env-shaped noise, band-limited near 4 kHz.
  • Clap (clap|maracas): ~2 kHz dual band-pass noise through a VCA driven by the Service Notes' Figure-13 three-teeth sawtooth — the "multiple hands" transient — plus the Q70 reverberation tail.
  • Hats: one circuit, two envelope paths, and the hardware choke (Q23/R173): a closed-hat trigger terminates a sounding open hat, pinned by test. This is why tap.808.hat~ is one object with two inlets — the choke is unimplementable across separate externals.
  • Cymbal: the bank through both voicings with two separately enveloped bands (strike/ring/body), decay spanning the chart's 350–1200 ms.
  • Cowbell: just the 540/800 pair into the ~860 Hz voicing, two-slope envelope.
  • Toms/congas (@size × @model): the resonator at the chart tunings with the D80/D81 attack pitch fall; toms add a pink-noise layer (pinned by seed-sensitivity, since the diode bend's own harmonics defeat spectral separation); congas are the same circuit, no noise.
  • Rim/claves: the ~1667 + 455 Hz crack with the swing-VCA's tanh harmonics, versus the pure ~2500 Hz tick.

The family also carries per-channel summing gains (k_tomc_mix, k_cl_mix — the hardware's summing resistors into the mix bus): the bridged-T's impulse gain grows with fc·Q, and before the balance pass the high conga peaked at ~5.3 while other voices sat far lower. Every voice's full-accent peak now lands in a consistent ~0.3–1.0 band, pinned by test.

The calibration pass: what measurement actually changed

The family was calibrated against a real unit (s/n 103852) recorded from the individual outs with knob positions encoded in the filenames — a 0/2.5/5/7.5/10 dial grid, 116 samples — so the comparison ran per knob cell, with identical measurements (spectral-peak fundamental, −40 dB decay, power centroid) on both sides. The result is a clean split:

  • Frequencies: the schematics were right. Kick within 2.4 %, snare within 1.2 % (including the tone-max mode flip), toms/congas/cowbell/ claves within ~4 %. The kick needed no constant changed.
  • Time: the recordings won. Tom, conga, cowbell, and clap tails roughly doubled; the snappy was band-limited and re-enveloped; the rimshot re-voiced low-dominant; the cymbal's decay span and brightness corrected; the closed hat's brightness residual later resolved by the hats' sizzle blend.

Each header carries its calibration note with numbers and residuals. The lesson is worth stating as a rule: schematics get you the frequencies; recordings get you the envelopes — decay behavior hides in pot tapers, electrolytic tolerances, and aging that no schematic states.

The engineering ledger

  • One resonator class vs. per-voice filters. Eight voices reduce to ~4 blocks plus thin compositions only because the bridged-T class keeps the injected-path structure of the real network instead of collapsing to a generic biquad. The generality was free once the nodal analysis was done — and the kick's per-sample leg modulation required it.
  • Behavioral envelope generators. The kick's EG is modeled as fast rise / ~1.1 ms release rather than as its own transistor network — the paper's own simplification, adopted with its citation. Fidelity effort went where the analysis said it matters (the leg, the retrigger, the sigh), not uniformly everywhere.
  • The WDF door, left closed but unlocked. The flagged @circuit upgrade path (wave digital, the svf.h two-circuit pattern) remains gated on an A/B showing an audible delta the informed model misses. The DAFx-14 paper's own finding — device nonlinearity matters less than folklore claims — suggests the gate may never open, which would itself be a documented result.
  • Determinism everywhere. Seeded noise and seeded tolerance mean every render, test, and calibration measurement is reproducible bit-for-bit. The calibration pass would have been guesswork without it.

Checkpoint

One network class matching the published transfer functions exactly and solved on its states so a time-varying resistor costs nothing; a metal bank whose ±20 % spread is a measurement, not a vibe; voices that are thin compositions with schematic-designated constants; and a per-knob-cell calibration pass that confirmed the frequencies, corrected the envelopes, and wrote its residuals into the headers it changed. Four ideas, eight voices, every number traceable.

Time as a function of phase: step_seq.h

The sequencer header is the smallest DSP file in the kernel and the one whose central decision does the most work per line: the engine owns no clock. It is handed a phase — a number in [0, 1) meaning "here is where we are in the pattern" — and everything else (the current step, whether this sample is a boundary, how far through the step we are) is derived from it, statelessly, every sample. This appendix explains why that one decision buys sample accuracy, polymeter, scrubbing, and drift-free multi-row lock for free, and then walks the three pieces built on it: the swing warp, the two emitters, and quantized recall.

Verification lives in two places: tests/step_seq_test.cpp (19 Catch2 scenarios, including a pairing test against the real tb303_voice.h) and the executed step_seq.ipynb. The design of record is plans/tap.seq.md in the Max package repo.

Deriving the step, in O(1)

Ignore swing for a moment and the whole clock is one line:

k = floor( wrap(phase) · length )

Swing delays each odd-numbered step's start by swing/2 of a step, so the start of step k is

start(k) = ( k + (k odd ? swing/2 : 0) ) / length

and the derivation gains one correction: compute the naive k, and if it is odd but the fractional position hasn't yet reached swing/2, the sample still belongs to the (even) step before it. Two comparisons, no search — the boundaries are monotone, so the correction is exact.

A step entry is simply k != k_previous. That definition, rather than "the clock ticked," is what makes the engine indifferent to how the phase moves: run it backwards and entries still fire (pinned by test); jump it and the landing step fires once; feed it a constant and nothing happens after the first sample. reset() just forgets k_previous, so a transport start fires its downbeat.

Time as a function of phase, drawn: a phase ramp through the floor derivation and step-entry inequality into the two emitters, plus two row lengths reading one ramp without drift

The one decision the file turns on — and polymeter falling out of it as arithmetic.

Why phase, not a pulse clock

The alternative — count incoming clock pulses — is how most step sequencers are built, and every one of them then grows a reset input, a position protocol, and a drift story. Deriving from phase dissolves all three:

  • Sample accuracy is inherited from the phase source. The notebook measures trigger edges landing within one sample of the analytically computed boundaries — the one sample being float rounding at the boundary itself, not accumulated error.
  • Multi-row lock is structural. Two rows fed the same ramp cannot drift, because neither owns any timing state that could drift. Mute one for an hour; it re-enters in place.
  • Polymeter is arithmetic. A length 12 row against length 16 rows off one ramp divides the same cycle differently — 12 and 16 entries per cycle, measured. The TR-808's triplet "pre-scale" falls out as a special case.
  • Position is explicit. Scrubbing, reversing, and jumping are the caller's choices about the ramp, not features the engine implements.

The cost is honest too: the engine cannot free-run. That is deliberate — phasor~ (transport-locked or not) already exists, and a sequencer that owns tempo is a sequencer that fights the transport.

Position within the step, and the gate duty

The tick also reports pos — the fraction of the current step's actual (swung) span elapsed — computed from the same start() function. Gate timing hangs off it: the note row closes its gate at pos ≥ 0.5, the pinned Open303 duty. Measuring duty against the swung span rather than the nominal step means gates never collide however hard the swing is pushed.

The trigger row: an impulse and a re-arming gap

trigger_row is the small emitter: on entry to a sounding step, emit the step's velocity for one sample (or pulse_ms worth, for envelope consumers), else zero. The single-sample default is a contract, not a simplification: every downstream tap.808.* voice re-arms its edge detector below 1e-3, and the test suite pins that two adjacent sounding steps produce two clean detectable edges. The header documents the one way to defeat this — a pulse_ms longer than a step merges back-to-back triggers — rather than silently preventing it.

The note row: a five-state sentence

note_row implements the tap.303~ contract, and its entire behavior fits in one paragraph of code. On entering step k: if the step is gated and its slide flag is set and a note is already sounding, change the pitch output and leave the gate level alone — that is legato, and the voice's RC does the glide. If gated without that condition, set the gate to 1.0 (2.0 if accented) — a fresh edge. If not gated, drop the gate. Between entries: close the gate at the duty point unless the next step is gated and slid — that look-ahead read is the gate-hold, and it is read live from the pattern each sample so an edit lands immediately.

Three edge cases are worth naming because the tests pin them:

  • Slide from a rest is a plain trigger — there is nothing sounding to slide from, so the flag degrades gracefully (the voice's note message behaves identically).
  • Chained slides chain — each held boundary defers the duty close to the next step, so a run of slid steps is one unbroken gate. Sixteen gated steps with three slide flags produce exactly thirteen note-ons, measured.
  • The wrap is a boundary like any other — a slide from step 15 into step 0 holds across phase 1→0, because nothing in the derivation treats the wrap specially.

One convention deserves its provenance note: the slide flag sits on the target step (the note being slid into), matching the package's note <pitch> [accent] [slide] message and the original interface dry-run. The hardware stores the flag on the source note ("slide to next"). The data models convert trivially — shift the flag column by one — and the divergence is documented in the header rather than discovered by a user.

Quantized recall: swap on the boundary sample

Patterns live in 16 slots. recall arms rather than acts (unless quantize now): the armed slot is applied on the next cycle entry (step 0) or step entry, and — the detail that keeps it exact — the engine then re-derives the current step against the new pattern's grid on that same sample, since the new pattern may have a different length. The notebook pins the semantics end to end: armed mid-cycle, the running pattern finishes its bar at its own amplitudes, and the first trigger after the wrap carries the recalled pattern's. That one message is the TR-808's A/B-half and basic/fill switching.

What is deliberately absent

No randomness (bit-exact by construction, still pinned by test, because invariants that aren't tested rot). No allocation after prepare() — the pattern store is a fixed 64-step array times 16 slots. No run/stop, no direction modes, no ratchets: the first two belong to the phase source, and the last is a future emitter, which is the point of the next paragraph.

The engineering ledger

  • Engine/emitter split. The clock math lives once; trigger_row and note_row are each a screenful. A future row flavor — CV, probability, ratchet — is another emitter, not another clock. This is also why the Max-side question "one generic object or two family objects?" could be answered by product taste rather than by implementation cost.
  • Look-ahead vs. cached hold. The gate-hold could cache "next step slides" at entry; reading it live costs one array access per sample and makes pattern edits take effect mid-step. Cheap beats stale.
  • Sample-resolution boundaries. Sub-sample trigger placement (fractional edge amplitudes à la BLEP) was considered and declined: the consuming voices detect edges at sample resolution, so sub-sample machinery would add complexity no consumer can observe. If a future voice interpolates its trigger time, the tick already carries the information needed to add it.
  • The armed-recall re-derivation. The subtle bug in naive quantized recall is applying the swap after deriving the step, leaving one sample computed against the old grid. Applying, then re-deriving within the same call, is two extra lines and the difference between "exact on the wrap sample" (measured) and "usually fine."

Checkpoint

A sequencer that is a pure function of phase plus a pattern: one line of derivation, one comparison for swing, entry as inequality — and from that, sample accuracy, polymeter, reversibility, and drift-free lock without a clock to maintain. The rows translate steps into the two shipped voice contracts, with slide as a held gate and a live look-ahead; recall swaps patterns on the exact boundary sample. Nineteen scenarios and an executed notebook agree, and the most satisfying number in either is small: thirteen note-ons, for sixteen steps, three of which arrived without knocking.

Three ways to move a pitch: yin.h, psola.h, pvoc.h

The user-facing chapter promised that three interchangeable engines land the same intonation. This appendix is about why that is hard: each engine is a claim about what a pitched sound is, and each claim fails somewhere specific and measurable. Two of those failures were found the good way — as failing tests during development — and both are now pinned as contracts rather than patched into vagueness.

These three headers live in the shared DspTap repository (the same home as the real FFT that machine/spectral.md describes), because a pitch detector and two shifters are not Max material or even TapTools material — they are primitives, in the fft.h mold: a double-precision golden model, a float32 embedded profile pinned against it, allocation-free noexcept processing, fixed documented latency, and hot loops kept contiguous as future Helium/HVX backend seams. Every number below is produced by the shipping code through DspTap's C ABI in notebooks/pitchshift.ipynb, and gated in test_yin.cpp / test_psola.cpp / test_pvoc.cpp.

A period is a lag that explains the signal: yin.h

Autocorrelation says: a signal is periodic at the lag where it best matches itself. The trouble is that a harmonic-rich signal matches itself rather well at twice the true period too, and "rather well" wins often enough to make naive autocorrelation an octave gambler. YIN (de Cheveigné & Kawahara, 2002) replaces "best match" with "smallest failure": a squared difference function

d(τ) = Σ (x[j] − x[j+τ])²,   j over the integration window

then divides each lag's failure by the running mean of all failures up to that lag — the cumulative-mean normalization — so d′(0) ≡ 1 and small lags stop being free wins. The detector takes the first lag whose normalized failure dips under an absolute threshold (0.1 by default), descends to the local minimum, and refines it with a parabolic fit over the three surrounding values — the sub-sample step that turns an integer lag grid into a fractional period.

The contract, measured: worst sine error 0.17 cents across 82–988 Hz (including deliberately non-integer periods), worst sawtooth error 0.155 cents with no octave errors — the trap the normalization exists to disarm. Noise and silence report unvoiced, and the threshold gates honestly (a deliberately dirtied sine flips to unvoiced when the threshold is tightened below its measured aperiodicity).

One honest limit, kept on purpose: first dip under threshold scans from short lags to long, so on synthetic material whose fundamental is nearly absent — a formant bump with almost no energy at f₀ — a subharmonic lag that happens to land on an exact integer can dip deeper than the true period's slightly-off-grid dip, and the detector follows it. The notebook demonstrates this deliberately and measures such material with a cepstral oracle instead. Real voices keep enough fundamental that the rule holds; the failure is documented, not hidden.

Finding 1 — a shifter that moves everything except the envelope: psola.h

TD-PSOLA's move is disarmingly physical. Put an analysis mark every period. Cut a two-period Hann grain around each mark. To synthesize a new pitch, lay the grains back down at a new spacing — period/ratio — and sum. The windows are arranged to sum to one at the identity, grains are scaled by 1/ratio to keep the sum flat elsewhere, and synthesis marks are placed with sub-sample precision (each grain resampled through the same 4-point Hermite kernel the rest of the family uses) so mark rounding never becomes pitch jitter. The file adds one real-time honesty: marks come from a free-running period-synchronous scheduler, not glottal-epoch estimation — the standard practical simplification — and the caller supplies the period, so detector and shifter stay independently testable.

Then the finding, told as it happened. The first shift-accuracy test fed the shifter a pure sine at ratio 2 and got back 0.0000 — silence, from a correct implementation. Because that is what PSOLA does: re-spacing period-synchronous grains resamples the source's spectral envelope at the new harmonic spacing. A voice's envelope is wide — formants — so the new harmonics sample it fine, which is exactly the celebrated property: formants stay put while pitch moves. A pure sine's envelope is a single spike at f₀, and after an octave up the new harmonic grid (2f₀, 4f₀, …) contains nothing at f₀ — the output honestly, correctly vanishes. One property, two faces.

The response was not to patch the algorithm into something less itself. The shift tests were rewritten onto voice-like material (a normalized band-limited sawtooth: ±8 cents across ratios 0.5–2.0 at healthy level), and the pure-tone behavior got its own pinning test, PureToneOctaveUpThinsOut, so that if this property ever changes, someone is forced to explain why. The header now opens with the warning label: know what PSOLA is; feed it harmonics; for pure tones use a waveform-preserving shifter.

Latency is fixed at 2·max_period + 2 samples — the price of grains that must be fully received before they can be laid back down.

Finding 2 — the naive phase vocoder loses half its level: pvoc.h

The textbook pitch shifter looks like four honest lines: STFT with Hann windows at 4× overlap; per-bin instantaneous frequency from the frame-to-frame phase increment; remap each analysis bin k to synthesis bin round(k·ratio); accumulate each synthesis bin's phase at its scaled frequency and inverse-transform. It is in tutorials everywhere. Measured on a unit sine, it delivers 0.14–0.46 of the input level at fractional ratios — more than half the signal simply gone — and its "identity" at ratio 1 is a sine of the right frequency with the wrong waveform.

Two structural reasons. First, a single partial does not live in one bin; it lives in a Hann mainlobe pattern across four-ish bins, and round(k·ratio) scatters that pattern (220 Hz × 1.5 lands on bins {5, 6, 8, 9} — nothing at the true target, 7.04). Second, free-running per-bin phase accumulators destroy the phase relationships across the lobe, and the overlap-add — which is a resampling filter with real opinions — partially cancels what remains.

The shipping design is Laroche–Dolson peak-region shifting. Find the spectral peaks (local maxima over ±2 bins, gated 80 dB below the frame's strongest bin so the noise floor cannot claim regions). Split the spectrum into regions around them. Translate each region rigidly by an integer bin offset — the lobe pattern survives intact, phase relationships and all — and rotate the whole region by a single accumulated per-hop phase ψ.

And here is the bug that cost an afternoon and earned its own comment block: ψ must accumulate the full per-hop frequency difference, ψ += 2π·hop·f·(r−1)/N, not the sub-bin residual left after the integer shift. An integer bin shift is implemented, in effect, by a modulator e^(2πi·shift·n/N) — but n is the frame-relative sample index, so that modulator restarts every frame and contributes nothing to frame-to-frame phase advance. Subtract the shift from ψ (the "obvious" refinement) and every frame disagrees with the last about where the shifted partial's phase should be; the overlap-add quietly shreds the signal. With ψ carrying the full difference, the measured contracts land: sub-cent frequency accuracy at every tested ratio, ~0.95 level everywhere, and — because at ratio 1 every shift and every ψ increment is exactly zero and the analysis phases pass straight through — exact waveform identity, one frame late, to 7.8 × 10⁻¹⁶.

The envelope as a filter: LPC formant preservation

set_formant(true) adds the classic source-filter correction. Per analysis frame: autocorrelate the windowed time frame to lag 48, run Levinson–Durbin (always in double — an order-48 recursion in float32 is not a place to economize), and evaluate the prediction polynomial's magnitude over all bins with one extra FFT of its coefficients — the same transform engine, one more call. That gives a spectral envelope E(k) = 1/|A(e^jωk)|, and every relocated bin trades envelopes: content moving from bin k to bin j is scaled by E(j)/E(k) (clamped to ±24 dB so a near-zero envelope cannot mint gain). The excitation moves; the envelope stays. Measured: a synthetic 800 Hz formant on a shifted-up-a-fifth voice stays at 800 Hz with the flag on (band-energy ratio 62:1) and dutifully chipmunks to 1200 Hz with it off. At ratio 1 the correction is E(k)/E(k) — exactly unity — so the identity contract survives the feature untouched. The method is implemented from the published literature only, which in this corner of DSP is a policy statement, not just a citation habit.

The house pattern

All three files repeat the fft.h discipline because it keeps paying: basic_*<Sample> templates with double as the golden model and float as the embedded profile, cross-precision agreement pinned by tests; geometry fixed at construction and every buffer allocated there; noexcept, allocation-free processing; latency as a number in the header, not a vibe; and the expensive inner loops (YIN's difference function above all) written as plain contiguous arithmetic so a Helium or HVX backend can slot in behind the same contract with the scalar build remaining the oracle.

Checkpoint

A detector that measures failure-to-match instead of match, normalized so short lags stop cheating, refined below the sample grid — sub-cent, octave- safe, honest about the one synthetic that fools its first-dip rule. A grain shifter whose deepest property — resampling the spectral envelope — is both its celebrated feature and its pure-tone failure, pinned from both faces. A phase vocoder that works because peaks move as rigid families with one phase register each, carrying the full frequency difference — since the integer shift's modulator restarts with every frame. And an LPC envelope trade that lets the excitation move while the mouth stays. Three claims about what a pitched sound is; three sets of receipts.

The nearest allowed note: tune.h

The pitch-primitives appendix built the parts: a detector and two shifters, each with a numeric contract. This appendix is about the composition — tap::tools::tune::corrector, the object behind tap.tune~ — where the interesting problems are not algorithms but policies: what runs when, what is allowed to allocate, what happens when the detector reports nothing, and one measured surprise that became the kernel's best war story. The scenarios in tune_test.cpp drive the class directly, using the DspTap detector as an independent pitch oracle on the output; notebooks/tune.ipynb re-measures the headline claims through the C ABI.

The pipeline and its clock

process() runs per sample; analysis runs per hop (256 samples at 48 kHz, about 5.3 ms, scaled with the rate). Each sample: feed the detector's input ring, maybe analyze, advance two slews (the applied correction and the grain window), compute the ratio, resynthesize.

The geometry trick that keeps every setter real-time safe: the detector is built at prepare() for the worst case — lags from 2 kHz down to 55 Hz — and the user's set_range() merely filters results afterward, treating out-of-range estimates as unvoiced. Changing the range never reallocates, so it is safe mid-audio, and the price is a fixed analysis cost: with window = τ_max = 873 samples at 48 kHz, the YIN difference function is roughly 760k multiply-adds per analysis — an ~80 µs scalar spike every 5.3 ms, well inside a 64-sample vector's 1.3 ms budget, and the FFT-accelerated difference function remains available behind the same contract if an embedded target ever objects.

Analysis converts the detected period to MIDI, chooses a target (next section), sets the correction goal in semitones, and retargets the grain window. Unpitched frames set the correction goal to zero — the corrector relaxes toward honesty — while the window holds its last value rather than lurching toward a default.

tap.tune~'s pipeline as a diagram: the per-hop YIN/target/glide/ratio brain over the per-sample ring and resynthesis backend seam

The pipeline and its two clocks — the dashed region runs per hop, everything else per sample.

The period lock, told as a bug hunt

The first version of the resynthesis stage was the two-tap tap.shift~ engine with its grain window clamped to a sensible fixed range, minimum 5 ms. The oracle tests immediately failed — not wildly, musically: a 452 Hz input hard-snapped to A440 came out at 441.4 Hz, 5.4 cents sharp. Detection was exonerated first (0.06 cents), then the applied correction (right to five decimals). The bias lived in the shifter itself, and an isolation experiment found the shape of it:

grain windowmeasured outputerror
exactly 2 detected periods (212.4 smp)439.98 Hz−0.06 cents
fixed clamp (240 smp)441.38 Hz+5.41 cents
480 smp (≈4.52 periods)441.36 Hz+5.34 cents

The two taps ride the same phasor half a cycle apart, so they sit window/2 samples apart in the delay line. When window/2 is an integer number of source periods, the taps read the same phase of the waveform and their crossfade is invisible — and the average retune ratio is exactly the phasor's ratio. When it isn't, every crossfade splices a phase jump into the output, and the jumps do not average away: they bias the pitch. Period-locking the window is not a quality nicety; it is what makes the ratio true.

The fix: the window targets the smallest even multiple of the detected period that clears the minimum — 2 periods normally, 4 for high pitches whose 2 periods would be under 5 ms — so the taps always sit an integer number of periods apart. The clamp survives only as an outer bound. This is the cleanest example in the book of a defect no assert-on-internals test would ever catch: only an oracle — the detector listening to the output — could hear 5 cents.

Choosing the target

Scale mode: round the detected MIDI to a center note, then scan offsets −6…+6 for enabled pitch classes, keeping the candidate nearest to the fractional detected pitch (ties resolve to the smaller motion). A tritone of search radius suffices for any non-empty mask; an empty mask returns no target, and no target means a zero correction goal — the object never guesses. MIDI mode is simpler and blunter: the nearest currently-held note in absolute MIDI space, whatever the distance (clamped to ±12 semitones of actual correction). amount scales the goal before the glide — a fader on the distance itself.

The glide

The applied correction chases its goal through a one-pole with time constant speed (0 = assignment, the hard snap), evaluated per sample so the goal can move every hop while the glide stays silky. The ratio is then 2^(applied/12), computed per sample; the exp2 is cheap and the alternative — caching with edge cases — is not. The grain window rides its own 15 ms slew toward the period-locked target, and the detected period gets a third slew for the PSOLA backend's per-sample period input. Three small slews, no zippers, no special cases at the joins.

Three backends behind one seam

set_backend() swaps only the last stage; detector, mapper, and glide are shared state that survives the switch. Both alternate engines are constructed at prepare() (PSOLA sized to the deepest detectable period, the phase vocoder's FFT scaled to ~21 ms at any rate), so switching allocates nothing and is safe mid-audio; the incoming engine is cleared to silence first — a fade-in, not a splice of stale buffers. The ledger, at 48 kHz:

backendresynthesislatency
grainperiod-locked two-tap (in-kernel)≈ base delay + window/2, a few ms
psolatap::dsp::psola2 × 873 + 2 = 1748 samples ≈ 36 ms
pvoctap::dsp::pvoc (+ optional LPC formant trade)1024 samples ≈ 21 ms

The backend-parametrized scenarios feed all three the same 46-cent-sharp sawtooth and require the same landing (±6 cents at healthy level); a switching scenario hops between engines mid-signal and requires finite output and a correction that is still standing at the end. set_formant() forwards to the phase vocoder — PSOLA preserves formants by construction and the grain engine is waveform-preserving, so the flag deliberately touches one path.

Learning the key

The auto-key learner is thirteen doubles and a policy. Every voiced analysis adds 1 to its pitch class's histogram bin; every analysis multiplies all twelve bins by a leak chosen so the histogram forgets with a 60-second time constant. On demand — never on a schedule — the histogram is scored by Pearson correlation against the published Krumhansl–Kessler major and minor profiles at all twelve rotations, and the best of the 24 becomes the estimate, with the winning correlation as confidence. A mass guard withholds any estimate until roughly half a second of voiced material exists, so silence cannot have an opinion.

The design decision that matters is that the learner is advisory: autokey_estimate() reports and autokey_apply() adopts, but nothing in the audio path ever re-aims the targets on its own. This is a UI-safety argument, not modesty — a corrector that changes its own scale mid-phrase turns a wrong estimate into a wrong performance, and the person at the patch cannot undo what they never saw happen. Measured: a tonic-weighted D-major scale scores D major at 0.95 confidence; an A harmonic-minor melody scores A minor; reset withdraws the estimate.

The ledger

  • Allocation discipline. Everything sized at prepare(): detector frame and ring, both alternate backends, the grain buffer at the maximum window. After that the audio path allocates nothing; every setter either writes a double, flips a flag, or clears preallocated state.
  • Oracle-based testing. The scenarios measure the output's pitch with the independently-certified DspTap detector — the only kind of test that caught the period-lock bias — and use sawtooth, not sine, wherever PSOLA participates, per its documented material contract.
  • The maxtest. One assertion runs inside a real Max: unpitched DC in, exactly DC out — detector unvoiced, correction zero, complementary envelopes summing to one. It pins the whole "never guess" policy at unity gain in the shipping binary.
  • What the wrapper adds. Only plumbing: attribute forwarding, the atomic pitch handoff to a scheduler timer for the right outlet, and applykey writing back through the attributes so Max's saved state stays the source of truth.

Checkpoint

A per-sample corrector with a per-hop brain: worst-case geometry bought at prepare() so nothing ever allocates again, unpitched input relaxing to zero correction, and three slews smoothing every join. The war story is the period lock — two taps half a window apart are only honest when that half-window is an integer number of periods, and only an oracle test could hear the 5-cent lie. Targets are chosen, never invented; the glide is one pole and one exp2; the backends swap behind a seam that clears to silence; and the key learner watches, scores, remembers for a minute — and speaks only when spoken to.

The clipper in the loop: overdrive.h

The user-facing chapter claimed that tap.overdrive~'s gain tilts with frequency and that the tilt grows with drive — behavior a memoryless waveshaper cannot produce. This appendix derives the loop that produces it, shows why the obvious implementation of that loop is a stability bomb and how the file defuses it, and records the design decisions — shaper choice, asymmetry mechanics, oversampling versus ADAA — with the alternatives they beat.

The design brief was not a schematic (none is published for the Little Green Wonder, the listening reference): it was the class of TS-lineage feedback overdrives. The honest statement of the goal, from the project's handoff notes: the interesting part is not the transfer curve — it's the frequency-dependent gain and the softer, never-fully-flat knee that a feedback clipper gives you.

The topology, and what it must do

In a TS-lineage pedal the diodes sit in the feedback path of a non-inverting op-amp stage whose feedback network is frequency-dependent. Two consequences:

  1. The loop gain — and with it the effective clip threshold — varies with frequency: bass sees little gain and stays clean, mids see all of it.
  2. The output is input + limited feedback term: even at maximum drive the transfer's slope never reaches zero, because the clean input always passes.

overdrive.h models this with the minimal structure that keeps both traits:

w = shape( G·x − g_fb·LP(w) )      the clipper inside a lowpass feedback loop
y = x + w                          the unity clean path (non-inverting topology)

Signal-flow diagram: preamp and body pre-EQ into the oversampled region, where the gained signal meets a summing node, the shaper, and a lowpass feedback path; a unity clean path bypasses the shaper; DC block and body post-EQ follow at base rate

The whole kernel on one line. The red loop is the frequency-dependent gain; the amber path is why the transfer never flattens; everything inside the dashed region runs at the oversampled rate.

G is the drive gain (a dB sweep, +6 to +46). The lowpass LP (one-pole, corner 660 Hz) makes the fed-back signal predominantly low-frequency, so the negative feedback suppresses gain exactly where the pedal does. In the linear region (shape ≈ identity) the loop's small-signal gain is

w/x = G / (1 + g_fb·|LP(ω)|)

— at DC, G / (1 + g_fb); far above the corner, G. The file picks g_fb from a single voicing constant: g_fb = G/k_lf_gain − 1 with k_lf_gain = 2, which pins the low-frequency gain at +6 dB regardless of drive while the mids ride G all the way up. That one line is the measured headline — a bass-to-mid tilt of +5/+16.3/+17.2 dB at drive 0/0.5/0.9 — and it is the real-pedal behavior: turning up a TS makes the mids filthier while the low E barely moves.

Why the loop must be solved zero-delay

The naive discretization feeds back yesterday's lowpass state:

s    = G·x − g_fb·lp_state        // uses the previous sample's state
w    = shape(s)
lp_state += a·(w − lp_state)

That inserts a unit delay into a feedback loop — the same mistake as the Chamberlin SVF, with the same fuse. Linearize it: the state-to-state map has Jacobian J = (1 − a) − a·g_fb·shape′. At 48 kHz × 4 oversampling, a 660 Hz one-pole has a ≈ 0.021; at drive 0.9, g_fb ≈ 62. With shape′ = 1 (small signal — the quiet case!) J ≈ 0.979 − 1.34 = −0.36: stable, fine. But push g_fb higher — drive 1.0 gives G = 200, g_fb = 99 — and J ≈ 0.979 − 2.12 = −1.14. |J| > 1: the loop limit-cycles near Nyquist, audible as a parasitic whine that comes and goes with the signal level. A feedback clipper that oscillates when you turn it up is not a pedal, it's a bug report.

The fix is the house zero-delay move (svf.h's driven circuit, ladder.h's solver_fast): integrate the one-pole trapezoidally (TPT), solve the loop's linear part implicitly, then apply the nonlinearity and commit its output to the state. With the TPT one-pole v = (g·w + s)/(1 + g), substitute into the loop and solve for the node as if shape were identity:

w_lin = ( G·x − g_fb·s/(1+g) ) / ( 1 + g_fb·g/(1+g) )
w     = shape(w_lin + bias) − shape(bias)
v     = (g·w + s)/(1+g);   s ← 2v − s

No delay in the linear loop, so no delay-induced instability at any g_fb; and because shape′ ≤ 1 everywhere, the committed value only ever reduces the effective loop gain below the linear prediction — the approximation errs toward stability. At DC the solve gives w = G·x/(1 + g_fb) exactly, which is what makes the pinned-bass-gain arithmetic above exact rather than approximate. The kernel suite pins the consequence: after a full-drive, full-asymmetry signal stops, the output decays below 10⁻⁶ — no limit cycles.

The shaper: u/√(1+u²), and why not tanh

Three candidates from the brief, in the order they were rejected:

  • std::tanh — the reference softclip, and the expensive outlier: a transcendental call per (oversampled) sample that vectorizes badly.
  • Padé-style tanh approximations — cheap, but the usual forms are exact only on a bounded interval and go flat (or worse, retreat) beyond it — reintroducing the hard plateau this design exists to avoid, with a curvature discontinuity at the seam that aliases.
  • shape(u) = u/√(1+u²) — chosen: C∞ (no curvature seam to alias), strictly monotonic, asymptotic to ±1 but never flat, one multiply-add and one square root — which vectorizes as a reciprocal-sqrt instruction on every SIMD ISA this kernel targets, and reduces to a small LUT for a future fixed-point port.

The three candidate curves overlaid: the hard clip's corners, tanh's tighter knee, and the chosen rational curve's gentler, seam-free approach

The chosen curve reaches its asymptote more slowly than tanh — softer knee, lower-order harmonics — and unlike the hard clip it has no corner for the spectrum to pay for.

Asymmetry — the even-harmonic control the odd-only Jamoma curves structurally lacked — is a bias inside the shaper, output-corrected so silence stays silence: w = shape(u + b) − shape(b) with b = 0.5·asymmetry. At asymmetry 0 the whole path is an odd function and the measured H2 sits at the numerical floor (−151 dB); at 0.6 it is −26 dB and musically present. The correction term keeps the first-order DC out, but a biased clipper still rectifies: under signal it makes DC, and the feedback one-pole would happily integrate it. Hence the DC blocker after the clipper — y[n] = x[n] − x[n−1] + 0.9997·y[n−1], the Jamoma TTDCBlock constant kept for provenance — permanently in the path, not an option. (The original TTOverdrive instantiated that same blocker and then overwrote its output buffer without using it; the vestigial call was one of the tells, noted in the handoff brief, that the old code path was never going to be the base.)

Oversampling, not ADAA (for now)

Clipping generates harmonics without limit; everything past Nyquist folds back inharmonically. Two published remedies: oversample the nonlinearity, or antiderivative anti-aliasing (Parker et al., DAFx-16). ADAA is cheaper per dB of alias suppression, but its x[n] ≈ x[n−1] fallback branch is hostile to the branchless-SIMD constraint this kernel inherits from its embedded targets, and its difference quotient loses precision in single-precision float — a real concern for the fixed-point/f32 ports. So v1 oversamples: zero-stuff + 4th-order Butterworth anti-image up, matching anti-alias down — the ladder.h/svf.h resampler verbatim, self-contained per house rule. Factors 1/2/4/8, default 4×. Measured on a hard-driven 5 kHz tone: the folded seventh harmonic improves from −22 dB (1×) to −36 dB (4×) while the in-band harmonics stay within measurement error. ADAA remains the flagged experiment for after the voicing locks, so the comparison is apples to apples.

Output spectra of a 5001 Hz tone at drive 0.9, 1x overlaid on 4x: the 1x trace shows alias peaks standing tens of dB above the 4x floor

Every red peak standing above the blue mass is inharmonic fold-back the default 4× removes; the true harmonics (multiples of 5001 Hz) coincide in both traces. The dashed line marks the folded seventh harmonic the kernel suite pins.

The voicing layer, honestly labeled

Everything above is structure; the sound of the body control is a handful of constants (k_voice_* at the top of the file): the pre-clipper highpass corner sliding 40→320 Hz across the knob, the upper-mid bell at 1150 Hz (above the classic TS hump — the LGW's push sits higher), the +2.5 dB counterclockwise treble shelf, the fixed +1.5 dB mid seasoning. They produce the measured control shape (±10 dB at 100 Hz between extremes, +4 dB at the bell) and they are by-ear placeholders: the header says so, this book says so, and the numbers will move when the in-Max voicing pass against LGW demos happens. What will not move is where they live — all linear EQ outside the nonlinearity, because in the reference pedal that is what the Body knob is.

The parameter block is normalized on purpose

drive and asymmetry are 0..1, body is −1..+1; only preamp/output carry units (dB). The perceptual mapping (dB sweep of G, level compensation) lives inside the kernel, not in the knob range — so the parameters map directly to controllers, to live.dial, and to Q15/Q31 fixed-point registers on the Cortex-M targets this library's headers are written to reach. Parameters ride the standard per-sample linear ramps (default 20 ms); the derived coefficients — G, g_fb, the solve constants, the voicing biquads — refresh only on samples where a ramp actually moved, the same two-tier scheme as svf.h.

Everything in this chapter is executable: the loop math and stability claims are pinned by tests/overdrive_test.cpp (silence decay, tilt-grows-with- drive, even-harmonic emergence, DC blocking, alias improvement, determinism), every number is a cell in the verification notebook, and the measured figures are regenerated from the shipping kernel by book/figures/overdrive.py.

Wear as the stabilizer: tape_loop.h and discreet.h

Every regenerating loop in this library before these files made the same promise the same way: the loop is strictly contractive because feedback is capped below one (delay.h's k_fb_max = 0.99, the comb bank's calibrated ring time). tape_loop.h and discreet.h exist to make the opposite promise — regeneration at exactly 1.0, bounded anyway — and this appendix is the derivation of why that is allowed.

A shared header, by the house rule

The family needed the same four pieces twice (discreet.h and airport.h are both tape machines), and the reuse rule sorted them cleanly. Classes with state went into a shared header the way swing_vca.h was created for the drum family: tape::reel, tape::wow_flutter, tape::wear, and a tape::ramp that is a cited copy of delay.h's anti-zipper unit. Few-line expressions stayed copies-with-citation, as ever: the Hermite polynomial inside reel is the same read as delay.h, line for line, and says so; the saturator is not copied at all but included — vca::swing_shape, the shared swing-type stage, with the reason on the include line.

reel: one wrap, two topologies

A reel is position-addressed circular storage whose reads and writes wrap modulo a settable loop length, not the buffer size. That one decision lets the same class serve both kernels. discreet.h runs it as a delay line: loop length equals capacity, an integer write head advances forever (wrapped into range each sample — a bare long head would overflow LLP64's 32-bit long in half a day of audio), and the play head trails it by the loop span. airport.h runs it as a true loop: length set per piece, one free-running head, positions handed in raw because the reel does all modular arithmetic itself. A length change is deliberately a splice — content kept, positions re-wrapped — because that is what cutting tape does.

wow_flutter: periodic on purpose

The transport error is two sines — slow-deep wow, fast-shallow flutter — returning a read-position offset in samples, phases zeroed at prepare(). The periodic term is the dominant one in the tape-echo literature (Arnardóttir, Abel, Smith, AES 2008), but the deeper reason the stochastic term is a documented non-goal is testability: the wow promise is pinned by predicting peak pitch deviation in closed form (depth · 2π · rate, so 2 ms at 0.5 Hz ⇒ ±10.9 cents) and measuring it with the YIN oracle — 10.9 measured — and that oracle test only exists because two renders are bit-identical. Determinism was a design force here, not an afterthought.

wear: the boundedness argument

One pass of generation loss is three stages in fixed order: an exact one-pole darkening lowpass (1 − e^(−2πf_c/sr), the grm_comb.h map), the shared saturator swing_shape(v, d) = tanh(d·v)/d, and the normalized DC blocker. Each carries one clause of the proof:

  • tanh is bounded, so for any drive d > 0 the wear output can never exceed 1/d — whatever the loop has accumulated. That is BIBO stability at regen 1.0, unconditionally, from the saturator alone.
  • The DC blocker (pole 0.999, peak gain normalized to exactly 1 — the normalization grm_comb.h earned the hard way, chasing a +0.2 dB/s swell) kills the one frequency the lowpass would happily sustain forever with an offset attached.
  • The lowpass is strictly contractive above its corner and asymptotically transparent below it — which is not a leak in the proof but the musical contract: at drive 0 and regen 1.0 the sub-corner band sustains indefinitely, cleanly. The header calls this the Frippertronics contract and states it rather than hiding it.

So where delay.h proves stability by gain, this family proves it by shape: each pass survives because it is degraded. The pinned test drives regen 1.0 for ten seconds of ring and asserts non-growth — never decay, because decay would betray the contract just as surely as growth.

The doppler decision

discreet::machine gives loop_seconds an ordinary ramp and does nothing else, because nothing else is needed: moving a fractional read head is tape-speed doppler. A 0.5 → 0.75 s glide over half a second reads back an octave down mid-move (measured: 220 Hz, then re-lock within five cents) with no discontinuity, since position is continuous even where its slope is not. The rejected alternative — crossfading between two taps — would have hidden the machine, and hiding the machine is the one thing this kernel is for. The wow offset is clamped so the read can never cross the record head; at absurd depths on short loops the transport flattens against the clamp rather than wrapping, which the header files under honest limits.

A finding: the arithmetic agreed

The per-pass wear transfer is fully analytic — regen · |H_lp| · |H_dc| on the unit circle — so the notebook measured it the direct way: a two-tone burst (300 Hz under the corner, 6 kHz over it) recirculated at drive 0, each generation's tones read by Goertzel. Measured per-pass ratios: 0.292 and 0.890. Predicted: 0.292 and 0.890. Three decimals of agreement between a rendering kernel and a formula derived independently in the test is the cheapest kind of confidence this library knows how to buy, and both the test (with 15% and 5% tolerance bands it never needs) and the executed notebook carry the measurement.

The engineering ledger

The suite leans on four instruments. Analytic transfers wherever the path is linear (the per-pass darkening scenario asserts against the exact formula, both tones, both directions — highs die faster and lows barely fade, so the test cannot pass vacuously). Two-window RMS for long-run claims, inherited from the comb bank's swell story: regen 1.0 rings ten seconds and the late window may not exceed the early one. The YIN oracle for anything with a pitch: wow depth in cents against the closed form, the doppler glide and its re-lock. And bitwise assertions where the law is exact: mix endpoints, the first echo returning as literally the recorded impulse, two wow renders identical to the bit. The DC-step scenario checks the blocker's actual job — a held offset at regen 1.0 does not accumulate and the tail's mean returns below 0.02 — rather than a decay the contract never promised.

Checkpoint

One shared header, four blocks: a reel that wraps at the loop, a transport that is two deterministic sines, a wear stage whose tanh bound is the stability proof, and a cited copy of the house ramp. discreet.h composes them into the two-machine loop where regeneration legally reaches 1.0, loop moves are doppler because read heads are physical, and every claim is carried twice — discreet.ipynb executed, discreet_test.cpp pinned.

Free-running heads, one shared clock: airport.h

airport::loop_bank is structurally the smallest kernel in the family — a fixed array of loops, a stereo sum, no feedback anywhere — and that is what makes it interesting to read: nearly every promise it makes is structural, so nearly every test on it is bitwise. This appendix walks the file in code order and dwells on the one discipline that defines it.

loop_state: the multitap idiom with a reel in each seat

The bank is std::array<loop_state, k_max_loops> with an active count — delay.h's multitap shape, kept deliberately: per-index setters that silently no-op on a bad index, getters that return safe defaults, newly activated slots arriving at their stored settings. Each seat holds a tape::reel (its own worst-case buy — eight 30-second reels is ~92 MB of double tape, the family's largest allocation, stated in the header rather than discovered in production), a tape::wear used as a playback shade, a phase, a record flag, and three ramps (level, pan, darken).

The phase discipline

The load-bearing sentence in the header is "the phase is NEVER reset": recording starts wherever the head is, set_loops activates a loop with its head wherever it last was, a splice re-wraps the head modulo the new length without rewinding, and only prepare()/clear() — DSP restarts — may rewind. The reason is musical: in "2/1" the free-run is the piece, and any convenience reset (snap to zero on record, realign on length change) would quietly delete the composition. The pinned scenario earns the promise the blunt way: it fires a setter storm mid-render — level, darken, record, length, count — and then requires the click grid unmoved and the head advanced by exactly the samples processed. phase() exists as introspection precisely so that test could be written.

Record semantics

record is a gate, not an action: while on, the input replaces the tape at the integer head position, after the read — so you hear the previous generation under the head while punching, and one Hermite support point (two samples) of the old generation blends across the punch, which the header files under honest limits instead of papering over with a crossfade. No overdub-sum, because the provenance had none: each Airports phrase was recorded once. Freeze is the strong promise — record off, and two successive passes of the loop are required bit-identical. That promise is only possible because of the next decision.

The shade and its bypass

Per-loop darken reuses tape::wear with drive pinned at 0, as a static playback tone — deliberately not generation loss, because a frozen loop replays the same magnetic imprint every revolution and modeling wear on it would be dishonest physics. At the band ceiling (the default) the stage is bypassed entirely: not "flat enough", but not-in-the-signal-path, which is what upgrades the freeze test and the hard-pan test (a pan of −1 adds the loop's samples to the left bus unscaled) from tolerance checks to bitwise facts. Engaged, the shade is the exact one-pole from grm_comb.h, and the notebook measures a 6 kHz phrase through a 1 kHz shade at 0.169 of its transparent twin against 0.169 predicted.

composite_period_seconds

The lcm of the active loop lengths in samples, folded pairwise with a long long gcd, overflow detected before each multiply and reported as +inf. It is introspection, not DSP — but it is the piece's thesis as a number: 24000- and 30000-sample loops report exactly 2.5 s (and the pinned scenario also proves the rendered output repeats at 120000 samples and does not repeat at 60000), while seven airport-scale lengths overflow to infinity, which the header calls the point.

A finding: the raster before the assertion

The lcm scenario existed as an assertion first — bitwise equality of two 2.5-second windows — and it passed, which is exactly why it was worth plotting. The notebook's event raster (every return of loop A, loop B, and their sum on one timeline) made the same fact visible: the coincidence pattern audibly and graphically re-enters at 2.5 s and drifts everywhere short of it. The assertion pins the promise; the raster is what convinces a human the promise means something. The pair — one bitwise test, one executed figure — is this library's preferred way to hold a structural claim from both sides.

The engineering ledger

Almost everything here is exact, so the suite asserts exactly: bit-equality for freeze and for the lcm window, bitwise silence on the far bus for hard pans, phase() continuity to 1e−9 through the setter storm, and the splice law (0.9 of a 1 s loop re-wraps to 0.8 of a 0.5 s loop, never zero). The one measured tolerance in the file is the shade's analytic transfer at 20%, and the equal-power pan law needs no scenario of its own because the multitap chapter already pinned the center at 1/√2 to 1e−12 — same code shape, same law, cited rather than re-proven. Long-run behavior needs no stability test at all: there is no feedback path to go wrong, which is itself a fact the file's structure makes obvious enough not to test.

Checkpoint

A fixed bank of reels, one sacred free-running head each; record replaces and freeze is bitwise; splices re-wrap, never rewind; the shade bypasses to bit-transparency at the ceiling; and the composite period is the score's arithmetic made introspectable. The promises are structural, the tests are bitwise, and the executed raster in airport.ipynb is the human-readable proof that the structure composes.

Events, not audio: garden.h

garden::bed recirculates events where its siblings recirculate samples, which makes it the family's odd one out mechanically and its purest member conceptually: the wear-as-stabilizer inversion survives the abstraction jump intact, as arithmetic. This appendix walks the machinery — the ring, the split between planting and firing, the bell, the quantizer, the gardener — and the two contracts that had to be designed before they could be tested.

The event ring

Sixty-four fixed seats (std::array, nothing allocated at prepare() — this kernel buys no tape at all), each event a pitch, a velocity, a brightness, a position on the loop, and a plant-order sequence number. The sequence number exists for one policy: when the garden is full, the oldest live bloom yields to a new plant. The musical argument is stated in the header — a touch must always speak (rejecting input makes an instrument feel dead), and the oldest bloom has survived the most decay passes, so it is the quietest thing on the table; retiring it is the least audible edit available. The pinned scenario plants a distinctive high note, floods the ring with sixty-four more, and requires the first note's pitch measurably gone from the following pass.

Fire is not plant

note() does not sound a voice. It quantizes, seats the event at the loop's current position, and returns; the next process() sample finds the event's position under the playhead and fires it. The first draft did both — plant-and-fire in note() — and the loop fired it again one sample later, a double-trigger that fell out of the design the moment firing became the loop's exclusive job. One mechanism, two consequences: a plant sounds one sample late (inaudible, documented), and every sounding of every event goes through a single code path, which is what makes the return grid a testable promise. After each fire the event blooms: velocity times decay, brightness times soften, retire below floor — so a bloom lives exactly ceil(log(floor/velocity)/log(decay)) passes and the population converges no matter the planting rate. That is the stability theorem, and it is three lines of arithmetic instead of a saturator.

The bell

Two-operator FM at a fixed ratio of 3 (Chowning 1973), amplitude from the shared tr808::decay_env, modulation index velocity · brightness · k_index_max. The ratio was a test requirement before it was an aesthetic: an integer ratio keeps the spectrum harmonic, harmonic means the YIN oracle reads the fundamental, and the scale-contract scenario — plant off-scale pitches, require every sounded note on the scale within 20 cents — only exists because the voice is honest to a pitch detector. Softening maps to the index, so "purer every pass" is measurable as a Goertzel trajectory: the 4f sideband fades return over return while the fundamental holds. Steals re-aim: the pool's quietest bell gets trigger()ed with new targets while its envelope and phases free-run, so a steal glides where a reset would click; the decay_env was built for exactly this non-resetting retrigger, one family over.

Quantize at entry

The scale machinery is tune.h's 12-bit pitch-class mask idiom — the make_mask builder, the nearest-allowed search that never travels more than a tritone — copied with citation, not included, because tune.h reaches into tap::dsp for its detector and a garden should not link a pitch tracker to hold five scale presets. The masks themselves are plain public-domain scale theory, deliberately not any app's preset list. Quantizing at entry (rather than at fire) is the semantic choice: a scale change re-pitches nothing already planted, which keeps running gardens stable under live tinkering and makes the contract easy to state.

The gardener and the seed

Idle planting consumes the family RNG (tr808::white_noise, xorshift64*, the seed-folding and clear-reseeds contract) — and only idle planting does. That consumption discipline is load-bearing: the third leg of the seeded triad, "with the gardener disabled the seed cannot matter at all", is only true because a disabled gardener never touches the generator, so two beds with different seeds run bit-identical until the first idle draw. The suite pins all three legs, the way the tr808 voices taught: same seed bit-exact, different seed audibly different, seed irrelevant when the random feature is off. step_seq.h promises "no randomness anywhere"; this kernel is the deliberate counterpoint, and the triad is the bridge back to a reproducible test suite.

A finding: envelopes never reach zero

The return-grid scenario was first written the obvious way — the percussive test bell surely dies between returns, so the first nonzero sample after silence is the onset. It failed, instructively: decay_env's exponential tail crosses the 1e−12 hard-zero more than half a second after a "20 ms" decay, so there is no silence between returns, only −200 dB of not-quite. The fix was to stop pretending: an instant-attack bell, an amplitude threshold scaled to the expected return velocity, and a grid claim of "within 8 samples" — a sixth of a millisecond — with the comment explaining that a threshold on a sine sits a few samples into the cycle. The lesson is general for this library: exponential envelopes make "silence" a tolerance, and tests that assume literal zeros between notes are wrong even when they pass.

The engineering ledger

The suite measures the output, never the internals: peak-per-window ratios for the decay staircase (0.5 ± 0.075 across four returns, then active_events() == 0 and the render below 1e−6), a strictly-decreasing Goertzel sideband for softening, YIN for the scale contract, the seeded triad rendered three times over, and structural bounds exercised at their edges — sixty-five plants against sixty-four seats, thirty-two notes against sixteen bells, finiteness and the k_voices amplitude bound under sustained stealing. The two introspection counts (active_events, active_voices) exist, as phase() does next door, so those scenarios could be written against public surface.

Checkpoint

A fixed ring of events fired by a loop counter into a fixed pool of FM bells: plant and fire kept strictly apart, wear as per-pass arithmetic (decay, soften, floor) with convergence as its theorem, scale masks copied from tune.h and applied at entry, and a gardener whose RNG discipline makes generative behavior compatible with a bit-exact test suite. Third costume, same inversion: the system stays bounded because everything in it is always fading. Every claim lives twice — garden.ipynb executed, garden_test.cpp pinned.

How to read a recipe

The first parts of this book keep two promises: the object chapters say what each tool is for, and the machine chapters say why to trust it. This part makes a third kind of promise. A recipe puts several objects on one patch cord and chases a specific sound — a record you have heard, an instrument you have coveted — and tells you honestly how close the kit gets.

Recipes are held to the house rules, adapted:

  • Every knob named exists, spelled the way the attribute is spelled. A recipe is checkable against the reference pages; if it says @decay 0.8, that attribute takes that value on the shipping object.
  • Settings are starting points, not measurements. A recipe's numbers get you into the neighborhood; your ears walk the last block. Where a chapter number is a measurement (a decay time, an alias floor), it still cites the executed notebook or pinned test that carries it — the recipes borrow those numbers rather than re-deriving them.
  • Provenance stays honest. When a recipe chases a record, it says what is documented about how that record was made and what is folklore. When it chases an instrument, it leans on the same published analyses the kernels were built from. What a recipe never does is claim to be the record — mix, room, tape, and hands are not in the box.
  • Every recipe ranks its ingredients. The house habit from the Moog recipe in the oscillator chapter: list what each element buys, in order of importance, so you know what to cut first when CPU or taste says so.

Each recipe has the same skeleton: the sound and where it came from, the signal chain, the settings (tables for knobs, grids for patterns), what each ingredient buys, and — because every tool is sometimes the wrong tool — when to leave the recipe and cook something of your own.

One machine, four decades

The TR-808 sold poorly, was discontinued in 1983, and then spent forty years becoming the most influential drum machine ever built — not by being realistic, but by being itself in four different genres' hands. This recipe visits four of those hands: the 1982 electro of "Planet Rock," the same year's slow soul of "Sexual Healing," the tuned-kick boom of Miami bass, and the half-time rolls of trap. Same eight circuits every time; what changes is the pattern, the accents, and which knob someone dared to turn all the way up.

One honesty note before the first grid: these patterns are starting points, not transcriptions. Where a record's production story is documented, the recipe says so; the grids themselves are the versions ears agree get you into the neighborhood, and your ears finish the trip. The voice knobs, on the other hand, are exact — every attribute below is spelled as the shipping object spells it, and the calibration numbers behind the voices live in the drum machine chapter and the tr808_calibration.ipynb notebook.

The scaffold every recipe shares

One phasor~ is the transport; every tap.808.seq~ row reads it; every row's output cable is a voice's trigger input. The phasor's frequency for a 16-step bar of 4/4 is BPM ÷ 240 (four beats per cycle, four sixteenths per beat). Rows fed the same ramp are sample-locked forever — that is the sequencer's phase-derived design (see its machine chapter), and it is why nothing below mentions sync.

phasor~ (BPM/240)
   ├── tap.808.seq~  ──▶ tap.808.kick~   ──┐
   ├── tap.808.seq~  ──▶ tap.808.snare~  ──┤
   ├── tap.808.seq~  ──▶ tap.808.hat~    ──┼──▶ +~ ──▶ tap.limi~
   ├── tap.808.seq~  ──▶ (open) hat inlet 2┤
   └── tap.808.seq~  ──▶ tap.808.cowbell~──┘

Program a row with two lists: hits (which of the 16 steps sound, 1/0 per step) and accents (which sounding steps lean, 1/0 per step). An accented step emits the row's accented level (default 0.5), a plain step emits plain (default 0.01) — those defaults are the hardware's accent knob at noon, and they matter more than they look, because a voice's trigger amplitude is a voltage on the 4–14 V bus: an accented hit is punchier and differently voiced, not merely louder. Raise accented toward 1.0 when a groove should hit like the accent knob cranked. Single steps tweak with step <n> <velocity> (1-based), and each row's 16 slots (store/recall) hold your fills.

In the grids below, X is an accented hit, x a plain one, . a rest.

1982, the Bronx via Düsseldorf: the "Planet Rock" kit

The documented part: Afrika Bambaataa and producer Arthur Baker built "Planet Rock" on a rented TR-808, borrowing Kraftwerk's melodies, and its kit — dry kick, clap-snare backbeat, offbeat cowbell — became the electro sound. The orchestra stabs were a sampler's; everything percussive is the machine's.

step:    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
kick:    X . . . . . . x . .  x  .  .  .  .  .
snare:   . . . . X . . . . .  .  .  X  .  .  .
clap:    . . . . X . . . . .  .  .  X  .  .  .
closed:  x . x . x . x . x .  x  .  x  .  x  .
open:    . . . . . . . . . .  .  .  .  .  x  .
cowbell: . . x . . . x . . .  x  .  .  .  x  .
  • Tempo ≈ 129 BPM → phasor~ 0.5375.
  • tap.808.kick~: @decay 0.35 @tone 0.55 — the electro kick is short and clicky, not the boom (that comes later in this chapter).
  • tap.808.snare~: @tone 0.6 @snappy 0.7; layer tap.808.clap~ on the same backbeat row — the clap-plus-snare composite is half the sound.
  • tap.808.cowbell~ on the offbeats, @level 0.6. The drum machine chapter's line stands: more cowbell is a patching decision.
  • Hats: closed 8ths; the open hat answering just before the bar turns.
  • Fill: store 1 the main pattern, program the classic descending-tom fill (tap.808.tom~, @size highmidlow on three rows) into slot 2, and recall 2 a bar before the phrase ends — quantize cycle (the default) swaps it exactly on the downbeat.

1982, Ostend: the "Sexual Healing" slow jam

The documented part: Marvin Gaye programmed the TR-808 himself for "Sexual Healing," and it became one of the first major hits carried by the machine — proof in the same year as "Planet Rock" that the same circuits could whisper. The kit is soft, sparse, and riding the plain/accent distinction rather than density.

step:    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
kick:    X . . . . . . x . .  .  .  .  .  .  .
snare:   . . . . x . . . . .  .  .  x  .  .  .
closed:  x . x . x . x . x .  x  .  x  .  x  .
open:    . . . . . . x . . .  .  .  .  .  x  .
claves:  . . x . . . . . . .  x  .  .  .  .  .
  • Tempo ≈ 94 BPM → phasor~ 0.3917.
  • tap.808.rim~ @model claves — the high tick is the hook of the kit. @level 0.5 keeps it a seasoning.
  • tap.808.kick~: @decay 0.6 @tone 0.35 — rounder than electro, still polite.
  • tap.808.snare~: @snappy 0.35 @tone 0.4 — more drum, less noise.
  • Leave the sequencer's plain level at its 0.01 default and place accents sparingly; at this tempo the difference between a 4 V hit and a half-accented one is the entire feel.
  • A touch of @swing 0.15 on the hat row loosens the grid the way a human thumb on the start button did.

Late eighties, Miami: the kick is the bassline

Miami bass turned the kick's decay knob to the top and discovered the 808's bass drum is a tuned instrument — a bridged-T resonator whose fundamental sits near 49 Hz (measured within 2.4 % of a real unit across the knob grid; see the calibration pass in the drum machine chapter). Turn decay up and it rings for seconds; give two copies two tuning ratios and you have a two-note bassline.

step:            1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
kick A (root):   X . . . . . . . . .  X  .  .  .  .  .
kick B (fourth): . . . . . . X . . .  .  .  .  X  .  .
snare:           . . . . X . . . . .  .  .  X  .  .  .
closed:          x . x x x . x x x .  x  x  x  .  x  x
  • Tempo ≈ 126 BPM → phasor~ 0.525.
  • Two tap.808.kick~ objects, two rows. tuning is a ratio of the stock fundamental, so target Hz ÷ 49 ≈ your setting: kick A @tuning 1.0 (G1, where stock already sits), kick B @tuning 1.33 (≈ C2, the fourth). Both @decay 1.0 — the whole genre is that knob at the top.
  • @tone 0.2 keeps the click out of the way of the ring; @attack 0.5 softens the punch mechanism if the notes should bloom instead of hit.
  • tap.808.snare~ @snappy 0.8 @drive 6 — the swing-VCA drive is the crack that cuts through the sub.
  • Watch the sum: two ringing kicks stack. tap.limi~ on the bus is the modern answer; riding level per voice is the period one.

The 2010s: trap, and the arithmetic of rolls

Trap keeps Miami's tuned, sustained kick and moves the snare to beat 3 — the half-time frame — then spends all its rhythmic budget on hi-hat subdivision games. Those games are where this sequencer's phase-derived design pays off: rows of different lengths off one phasor divide the same bar differently, so a 32nd-note roll row and a 16th-triplet roll row are just length 32 and length 24 — polymeter as arithmetic, measured in the sequencer notebook.

step:              1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
kick:              X . . . . . . x . .  x  .  .  .  .  .
snare:             . . . . . . . . X .  .  .  .  .  .  .
closed (len 16):   x . x . x . x . x .  x  .  .  .  .  .
roll    (len 32):  steps 25–32 hit, plain    (32nds on beat 4)
triplet (len 24):  steps 19–24 hit, plain    (16th triplets, beats 3–4)
  • Tempo ≈ 140 BPM → phasor~ 0.5833.
  • Tune the kick to the song's key with the ratio table: E1 ≈ @tuning 0.83, F1 ≈ 0.88, G1 = 1.0, A1 ≈ 1.12. @decay 1.0 @tone 0.15, and keep sigh at its default 1.0 — the pitch relaxation is the 808-bass glide everyone samples.
  • The roll rows: program hits only on their last steps (as above), leave them muted (@mute 1), and unmute for the bar that needs the roll — or keep separate patterns in slots and recall. Fast rolls do not machine-gun: the voices' filter states persist across triggers, so a roll interferes with the ringing tail like hardware (pinned by the family's tests; see the drum machine chapter).
  • Alternate hat voicing per unit: @seed is which 808 you own, and @tolerance 0.3 puts the metal bank's oscillators off-grid the way resistor variance really does (Werner et al. measured up to ~20 % — the chapter has the numbers). Two hat objects, two seeds, panned, is a stereo kit for free.

What each ingredient buys

  1. The pattern and its accents. Four decades of genre difference above is mostly the grids. The accent flags are not dynamics polish — they are the hardware's second voicing per drum. Spend your time here.
  2. decay and tuning on the kick. One knob separates electro from Miami; one ratio puts the kick in the song's key.
  3. The composite backbeat. Clap + snare on one row (electro), or snare drive (Miami, trap) — the backbeat carries the genre signature after the kick.
  4. Polymeter rows for rolls. Two extra rows, two length values, and the trap chapter of the machine's biography writes itself.
  5. seed/tolerance on the metal. Seasoning, in the salt sense: invisible until you A/B two units.

When to leave the recipe

  • You want those records, exactly. Mix, tape, room, and a human on the start button are not in the box; at some point the honest tool is the actual sample.
  • You want velocity-per-step expression. The velocities list gives a row continuous 0..1 levels — but note it trades away the two-level hardware model; the accent bus is the 808's own idiom.
  • You want 909, LinnDrum, or DMX. Different circuits, different machines — this family models one instrument, and its refusal to be generic is the point.

Three oscillators into a ladder

The oscillator chapter ends with the Moog recipe's core — the three-voice saw stack and the driven ladder — and ranks what each ingredient buys. This recipe finishes the instrument: the two envelopes, the amplifier, the gate, and the settings that turn one signal chain into the two patches everyone actually means by "Moog" — the bass that walks and the lead that sings. The model here is the classic three-oscillator monosynth voice: three oscillators into a mixer, one four-pole ladder, one loudness contour, one filter contour, glide on the pitch. Nothing below requires an object the package doesn't ship. And once the voice stands, the next recipe drives it at the records with names on them — Winwood, Worrell, Wright, Emerson.

Companion material: the oscillator chapter (the stack's rationale and the analog section's ranges), the ladder chapter (every filter number below is measured in its notebook), and the reference pages for tap.adsr~ and tap.vca~.

The voice, wired

pitch (midi note) ──▶ mtof ─┬─▶ tap.vco~ (voice 1)──┐
                            ├─▶ tap.vco~ (voice 2)──┼─▶ *~ 0.36 ─▶ tap.ladder~ ─▶ tap.vca~ ─▶ out
                            └─▶ ÷2 ─▶ tap.vco~ (3)──┘                    ▲              ▲
gate (0/1 signal) ──┬───────────▶ tap.adsr~ (filter contour) ─▶ *~ amount ─▶ +~ base ──┘│
                    └───────────▶ tap.adsr~ (loudness contour) ─────────────────────────┘
  • Pitch arrives as note frequencies (floats into each tap.vco~ left inlet; halve for voice 3's octave-down). The oscillators' own smooth ramp is the glide knob — no portamento object exists or is needed.
  • Gate is any signal that rises above tap.adsr~'s threshold and back — the envelope reads the gate by level, per sample. A tap.303.seq~ gate output (1.0 plain, 2.0 accented) drives it directly, which also gets you slides for free; so does a MIDI-driven 0/1 signal, or the trigger 1 / trigger 0 attribute messages for mouse-driven patching. The default mode analog gives the envelopes below the RC curves a Model D actually had; velocity (off by default) lets the gate's amplitude scale the hit.
  • The filter contour scales into the cutoff's signal inlet: envelope × amount (Hz) + base (Hz) into tap.ladder~'s right inlet. The classic panel's "amount" knob is your *~.
  • The loudness contour multiplies the ladder's output — tap.vca~ with the envelope into its gain inlet keeps the option of @circuit warm saturation later.

One period-correct honesty note: the original panel's contours are attack/decay/sustain with a release switch (release equals decay, on or off). tap.adsr~ gives the full four stages; set release equal to decay and you have the switch's "on" position.

The stack and the ladder

The three-voice table is the oscillator chapter's, reproduced so this page patches alone:

voicefrequencydetunedriftseed
1f−4811
2f+5822
3f ÷ 2+21033

All three: @shape 2 (saw), @jitter 3 @track 2 @imperfect 0.3, and smooth per the patch below. Sum through *~ 0.36 (≈ 1/2.8, headroom for three voices), then tap.ladder~ at the chapter's voicing: @mode lp24 @resonance 0.35 @drive 9 @asym 0.45 @comp 0.25. Keeping comp low preserves the authentic passband droop; drive 9 sits where the ladder notebook measures the tanh stages just starting to thicken (3.5 % THD at 8 dB). Spend the character budget in the filter first — the chapter's measurements are the argument.

Patch one: the bass

The left hand of a decade of records: short filter contour, no vibrato, glide short enough to read as punch rather than portamento.

controlsetting
all tap.vco~ smooth25 ms
filter tap.adsr~@attack 2 @decay 220 @sustain -18 @release 220
filter amount / base2500 Hz / 120 Hz
ladder resonance0.25
loudness tap.adsr~@attack 2 @decay 400 @sustain -3 @release 120

The sound lives in the filter contour's decay: 220 ms is the "wah" that articulates each note. Shorten toward 120 ms and it turns percussive; lengthen toward 400 ms and it turns brassy. For a rounder, more sub-friendly bass, drop drive to 3 and asym to 0.2 — the even harmonics are lovely on a lead and muddy on a bass amp. If anything downstream cares about DC, remember the ladder chapter's warning: an asymmetric saturator can leave a small signal-dependent offset — tap.dcblock~ after the VCA is one object of insurance.

Patch two: the lead

The singing version: longer glide, opened filter, resonance high enough to color but under the edge, and the release switch "on."

controlsetting
all tap.vco~ smooth80 ms
filter tap.adsr~@attack 15 @decay 600 @sustain -8 @release 600
filter amount / base4000 Hz / 300 Hz
ladder resonance0.55
loudness tap.adsr~@attack 8 @decay 300 @sustain -2 @release 350

Two moves push it from good to that sound:

  • Play the glide. 80 ms of smooth means overlapping note changes swoop; detached ones barely bend. The keyboard articulation is the vibrato.
  • Lean on the octave voice. Pull voice 3 up to f (unison) for the hollow reedy register, or leave it at f ÷ 2 and drop voice 2's level for the fat fifth-less stack. The interp-timed preset morph (store / recall <slot> <ms>) can glide between these voicings mid-phrase — a patch element the hardware never had.

What each ingredient buys

In order — and, per the house rule, cut from the bottom when CPU or taste says so:

  1. The stack. Three free-running voices at ±cents is most of the sound (the oscillator chapter's argument, with its measurements).
  2. The ladder. Drive, asymmetry, and the low-comp droop — the character budget.
  3. The filter contour. The one envelope listeners hear as "the synth's voice." Its decay is the most audible 100 ms in the patch.
  4. Glide. Free, iconic, already in the oscillator.
  5. The loudness contour. Keep it simple; the filter does the talking.
  6. The analog section. drift/jitter/imperfect at the chapter's moderate settings — salt, not sauce.

When to leave the recipe

  • You want polyphony. This is a monosynth voice; mc.-wrapping the whole chain gives you many monosynths, and a real polysynth patch wants per-voice envelopes and different discipline.
  • You want the 303 instead. The couplings that make acid are a different instrument — tap.303~ refuses to be decoupled, and that refusal is its chapter.
  • You want clean. Every stage here has an opinion — tap.svf~ and tap.fourpole~ are the polite siblings when the patch needs a filter, not a character.

The patches with names on them

The previous recipe built the three-oscillator voice. This one drives it at four records — a blue-eyed-soul hook, the bassline that retired a bass player, a singing art-rock lead, and the one-take modular solo that started it all — and, along the way, answers a fair question: if "Lucky Man" was played on a Moog modular, does the kit need a modular object?

The provenance rule from the part opener applies double here, because gear folklore is a genre of its own. For each patch the chapter says what is documented about the record and what is reconstruction. And the standing disclaimer stands: these settings chase the sound; the hands, the tape, and the mix stay on the record.

Every patch below is a delta against the wiring and tables of Three oscillators into a ladder — build that voice first. Two performance tools recur, so here they are once:

  • Vibrato is the oscillator's own now: @vibrato (depth in cents, so the musical width holds in every register), @vibrato_rate (Hz), and @vibrato_delay (ms) — the onset fades in through that time constant and re-arms on every new note, which is most of what makes a lead "sing." ±10 cents at 5.5 Hz with a few hundred milliseconds of delay is the classic setting. (This chapter's first draft had to print a scaling formula into the Hz-calibrated FM inlet here; that formula became the improvements plan's §2, and §2 became these attributes — the audit worked.)
  • Sequenced lines: tap.303.seq~ emits pitch as a MIDI-note signal and a gate at 1.0/2.0 — mtof~ turns the pitch into Hz for the oscillators' signal inlets, and the gate drives tap.adsr~ directly (it opens above 0.5). The Moog voice sequenced this way is the classic synth-line scaffold, slides included.

The Winwood hook — "While You See a Chance" (1980)

What's documented: Winwood played essentially everything on Arc of a Diver himself, synthesizers included; accounts of the rig put Moog monosynths at the center of it. The reconstruction: the opening hook is a brassy, open-filter lead with a fast attack and just enough glide to round the corners — a patch that sits between horn section and organ, which is very much a keyboardist's lead.

Deltas from the lead patch:

controlsetting
voices1 and 2 only, at f, detune −6 / +6; retire voice 3
all smooth40 ms
ladder@resonance 0.3 @drive 6 @asym 0.3
filter contour@attack 5 @decay 500 @sustain -6 @release 400, amount 4500 Hz, base 400 Hz
loudness contour@attack 5 @decay 200 @sustain -2 @release 250

The brass illusion is the filter contour's sustain sitting high (−6 dB): the filter opens and stays open, so the tone holds its brightness through the note instead of wah-ing. Play the hook in clean detached eighths — the 40 ms of glide only speaks when notes touch.

The bassline that retired a bass player — "Flash Light" (1977)

What's documented, and gloriously so: Bernie Worrell built Parliament's "Flash Light" bassline by stacking Minimoogs — the story is told with the number three attached — playing the line keyboard-style under Bootsy Collins' guitar. This is the patch where the previous chapter's "the stack is most of the sound" rule gets its funk citation.

Deltas from the bass patch:

controlsetting
voices1 and 2 at f (detune −7 / +7), voice 3 at f ÷ 2, its gain −6
all smooth35 ms
ladder@resonance 0.6 @drive 12 @asym 0.5 @comp 0.2
filter contour@attack 1 @decay 150 @sustain -24 @release 150, amount 2200 Hz, base 90 Hz
loudness contour@attack 1 @decay 250 @sustain -6 @release 100

The rubber is the filter contour: near-instant attack, short decay, and a sustain low enough (−24 dB) that every note is a squelch that immediately ducks. resonance 0.6 puts a vowel on the squelch; drive 12 into the tanh stages is the fat (the ladder chapter measures 16.5 % THD up there — that's the point). Play staccato sixteenths with octave pops; let the 35 ms glide smear only the connected passing notes. If the low end blurs, this is the one patch where comp earns its raise: 0.2 keeps some droop-era character while returning enough passband to anchor the root.

The singing lead — "Shine On You Crazy Diamond" (1975)

What's documented: Richard Wright's rig in the Wish You Were Here sessions included a Minimoog, and the singing synth lead lines in "Shine On" are credited to it. The reconstruction: a nearly clean patch — this lead's beauty is restraint, a barely-driven filter, and vibrato that arrives late.

Deltas from the lead patch:

controlsetting
voices1 and 2 at f, detune −2 / +2 — a shimmer, not a chorus
all smooth15 ms
ladder@resonance 0.15 @drive 3 @asym 0.2
filter contour@attack 30 @decay 900 @sustain -10 @release 700, amount 3000 Hz, base 250 Hz
loudness contour@attack 8 @decay 300 @sustain -2 @release 500

Then spend all your effort on the vibrato: @vibrato 10 @vibrato_rate 5.5 @vibrato_delay 400 — ten cents, arriving late, re-arming on each new note so held phrase-endings bloom while passing notes stay plain. The patch is deliberately close to the ideal oscillator — imperfect 0.2, drift at the polite end — because the expressive load is carried by the hands, and everything the analog section adds here it adds to sustained exposed notes.

The one-take solo — "Lucky Man" (1970), and the modular question

What's documented: Keith Emerson's solo on "Lucky Man" was played on his Moog modular system and famously kept from an improvised take — one of the first Moog solos on a rock record, and for a generation of listeners the first synthesizer they ever heard. The sound: a huge unison lead whose actual melodic content is mostly portamento — sweeps and dives across octaves, the glide circuit played as the instrument.

Deltas from the lead patch:

controlsetting
voicesall three; voice 3 up at f (unison), detune −5 / +4 / +7
all smooth280 ms
ladder@resonance 0.2 @drive 8 @asym 0.4
filter contour@attack 10 @decay 800 @sustain -4 @release 600, amount 5000 Hz, base 800 Hz
loudness contour@attack 10 @decay 300 @sustain -1 @release 400

At 280 ms of smooth, pitch is a place you travel to: hold a note, strike one two octaves up, and the voice draws the line between them. That is the solo. The filter stays essentially open (sustain −4 dB) because the record's drama is in pitch, not timbre.

So — does the kit need a Moog modular object? No, because you are holding one. A modular synthesizer is oscillators, filters, envelopes, and amplifiers with no fixed routing; the panel of patch cords is the product. In this package the modules are tap.vco~, tap.ladder~, tap.svf~, tap.adsr~, tap.vca~, tap.noise~, and the sequencer pair — and Max itself is the patch panel, with the routing freedom no hardwired monosynth voice (and no single "modular object") could offer. Everything Emerson's system did on that solo — voices summed to one filter, one loudness contour, glide on the pitch source — is the previous chapter's wiring diagram; what the modular added was the freedom to have wired it otherwise, and that freedom is the patching environment you are already in. The one genuinely modular idiom worth calling out is the sequenced line: tap.303.seq~mtof~ → the stack, gate → tap.adsr~, is the Moog-sequencer scaffold of the Berlin school and "I Feel Love"-era disco — no new object required, slides included.

What separates the four

The instructive part of putting these side by side: the signal chain never changed. What moved:

  1. The filter contour's sustain. High and it's brass (Winwood), open and it's drama (Emerson), low and it's rubber (Worrell). One attribute spans the genre map.
  2. smooth. 15 ms is articulation, 40 ms is rounding, 280 ms is the melody itself.
  3. drive and resonance. The funk patch is the only one leaning hard on both — and it's the one imitating three stacked instruments.
  4. The hands. Delayed vibrato, staccato versus legato, when not to play — the parts of the record the recipe honestly can't ship.

When to leave the recipe

  • You want the record's whole arrangement. The hook was never alone: Winwood's is doubled, Worrell's sits under a live band, Wright's floats on tape-delayed guitars. The patch is the voice, not the mix.
  • You want polysynth-era sounds. Prophets and Oberheims are a different architecture — per-voice envelopes on real polyphony — and imitating them with mc. stacks of this voice flatters neither.
  • You want the sequenced-modular genre. Start from the scaffold above, but that recipe deserves its own chapter — it lives in the plan file's backlog with "I Feel Love" written on it.

Move a knob while it loops

The documented origin story of acid house is an instruction manual for this recipe: in Chicago around 1985–87, Phuture (DJ Pierre, Spanky, Herk) let a secondhand TB-303 loop a pattern and turned the knobs while it played — "Acid Tracks" is twelve minutes of that. The lesson generalizes: an acid line is not a melody with a sound; it is a loop plus a hand. The pattern's job is to give the couplings something to chew on — accents for the bloom, slides for the vowels — and the performance is cutoff, resonance, and envmod moving in real time.

Everything measured here is borrowed from the acid machine chapter and its notebooks (tb303.ipynb, step_seq.ipynb).

The scaffold

phasor~ (BPM/240) ──▶ tap.303.seq~ ──pitch──▶ tap.303~ ──▶ out
                                  └──gate───▶   (right inlet)

One bar of 16 steps per phasor cycle (BPM ÷ 240, the drum scaffold's math); ~125 BPM → phasor~ 0.5208. The sequencer's pitch and gate outlets are the voice's own contract — accents ride the gate at 2.0, slides are pitch changes under a held gate, so the voice's ~60 ms RC does the glide.

A line to start from

Program per step (step <n> <pitch> [accent] [slide], rest <n>) or per lane. A serviceable opener in A — and, as everywhere in this part, a starting point, not a transcription:

step:    1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16
pitch:   33 33 45 33 33 36 33 31 33 33 45 47 33 33 31 36
gate:    x  x  x  x  .  x  x  x  x  x  x  x  .  x  x  x
accent:  A  .  .  A  .  .  A  .  .  A  .  .  .  .  A  .
slide:   .  .  .  .  .  .  .  S  .  .  .  S  .  .  .  .
pitches 33 33 45 33 33 36 33 31 33 33 45 47 33 33 31 36
gates   1 1 1 1 0 1 1 1 1 1 1 1 0 1 1 1
accents 1 0 0 1 0 0 1 0 0 1 0 0 0 0 1 0
slides  0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0

The ingredients that make it acid rather than bass: the octave jumps (33 → 45), at least one slide into a note (the flag sits on the target step), rests that let the filter close, and accents placed where the groove leans — not where the melody peaks.

The voice

recall 1 is the factory "squelch" and a fine start. Explicitly:

@waveform saw @cutoff 500 @resonance 0.9 @envmod 0.7 @decay 300 @accent 0.8

Then the moves, in the order a set builds:

  1. Ride cutoff. 300 → 2000 Hz over eight bars and back. This is the genre. Remember the modeled envmod law: 2/3 of the envelope's sweep sits above the knob, 1/3 below, and the resting point shifts as you turn it — the knobs interact like the hardware because the interaction is modeled.
  2. Stack the accents. Runs of accented notes at high resonance are the wow: the C13 capacitor doesn't fully discharge between close accents, and the measured cutoff-peak bloom across a run is ×1.94. Put three accents in a row somewhere and listen to the third one open.
  3. Raise resonance into the break. Past 1.0 is the documented bend territory — a stock 303 never self-oscillates, and neither does this filter until you push it there deliberately.
  4. waveform square for the hollow verse, saw for the drop.
  5. vca warm thickens exactly where the hardware does — measured 5.4 % difference signal on quiet notes, 11.5 % on hot accents.
  6. Transpose, don't re-program: transpose -12 on the sequencer for the sub-drop, +5/+7 for the question-answer sections. It shifts live, like the hardware's transpose mode without the mode.

After the voice

Acid techno's other instrument is the distortion pedal: tap.overdrive~ after the voice, driven hard, is the documented lineage (a 303 into a screaming feedback overdrive is half the harder end of the genre). Keep mute in reach on the sequencer for breakdowns — it drops the gate but the clock keeps running, so the line re-enters exactly in place.

When to leave the recipe

  • You're programming melodies. If the line only sounds right without slides or accents, it isn't an acid line yet — or it wants the generic bass rig (tap.vco~ + tap.svf~ + tap.adsr~) instead of this voice's refusals.
  • You want the filter alonetap.diode~ gives you the 303's ladder on any source, squelch and all, without the biography.
  • You want hands-free evolution. The 303 rewards a hand on a knob; if the patch must run itself, store extremes in the voice's preset slots and ride timed recall morphs instead — a different instrument, honestly.

The ostinato machine

Two documented lineages share one patch. The Berlin school — Tangerine Dream's Phaedra (1974) above all — put a Moog modular's step sequencer on stage and let a filtered ostinato run for twenty minutes while hands moved the cutoff. Three years later Giorgio Moroder and Donna Summer's "I Feel Love" built an entire hit from a sequenced Moog modular bassline. The Recipes part's Moog chapter argued you already own the modular — Max is the patch panel; this recipe is that argument cashed in: the sequencer pair driving the three-oscillator voice.

The scaffold

phasor~ (BPM/240) ──▶ tap.303.seq~ ──pitch──▶ mtof~ ──▶ slide~ ─┬─▶ tap.vco~ ──┐
                                  │                             └─▶ tap.vco~ ──┼─▶ *~ ─▶ tap.ladder~ ─▶ tap.vca~
                                  └──gate──┬─▶ tap.adsr~ (filter) ─▶ *~ amount ─▶ +~ base ──▶ ▲ (cutoff)
                                           └─▶ tap.adsr~ (loudness) ────────────────────────────▶ ▲ (gain)
  • tap.303.seq~'s pitch outlet is a MIDI-note signal; mtof~ turns it into Hz for the oscillators' signal inlets.
  • Its gate outlet (1.0 plain, 2.0 accented) drives both tap.adsr~ contours directly — the envelope opens above 0.5.
  • One honest wrinkle: tap.vco~'s frequency signal inlet bypasses the smooth ramp by design ("you are the smoothing") — so sequenced pitch steps land as hard steps, and slide flags in the pattern won't glide on their own. Put a one-pole slew (Max's slide~, or rampsmooth~) between mtof~ and the oscillators; the 303's ~60 ms RC is the reference feel. Short slew = articulation, long slew = the Berlin swoop.
  • The oscillator stack, ladder voicing, and envelope tables come from Three oscillators into a ladder — the bass patch is the right starting point. One voice instead of three is period-correct for the sequenced genre and cheaper; add the stack when the line is the whole arrangement.

The line

The genre's cell is small and the sequencer's phase math does the rest (one bar per phasor cycle; a length 8 row divides it into eighths — polymeter as arithmetic, per the sequencer chapter).

The octave bounce, "I Feel Love"-school — length 8, every step gated:

step:    1  2  3  4  5  6  7  8
pitch:   33 45 33 45 33 45 33 45
pitches 33 45 33 45 33 45 33 45
gates   1 1 1 1 1 1 1 1

The Berlin cell — length 16, a contour that repeats but doesn't resolve:

pitches 33 33 40 36 33 43 36 40 33 33 40 36 31 43 36 38
gates   1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Then the two moves that carry twenty minutes:

  1. Transpose, sparsely. transpose 0 → 3 → 5 → 0 at phrase boundaries is the harmonic language — one message, and the armed pattern semantics keep everything on the grid.
  2. Ride the filter, slowly. The loudness contour stays short and percussive; your hand (or a very slow LFO into the +~ base) opens the ladder over minutes, not bars. The ostinato doesn't change; the light on it does.

Settings that read as the genre

controlsetting
loudness tap.adsr~@attack 2 @decay 180 @sustain -12 @release 120
filter tap.adsr~@attack 2 @decay 160 @sustain -20 @release 160, amount 1800 Hz, base 150 Hz
ladder@mode lp24 @resonance 0.3 @drive 6
slew (slide~)short; raise it only for deliberate swoops
seq@swing 0 — the genre is a grid, and the delay does the humanizing

Two period tricks worth their lines: pan alternate notes (a length 8 row of accents driving tap.pan~ recreates the famous ping-pong doubling), and put an eighth-note tap.delay~ after the voice (@feedback 40 @mix 30) — the echo, not the sequencer, is where these records' motion lives. Since its rebuild the delay interpolates (Hermite) and regenerates through a DC-blocked loop; @interp 0 remains the bit-faithful legacy mode.

Glue: an 808 closed-hat row in 16ths from the drum scaffold, mixed low. Accents land in this scaffold too: turn up tap.adsr~'s velocity sensitivity and the sequencer's 2.0-amplitude accented gates hit the envelopes harder — the loudness contour for punch, the filter contour for the quack, or both.

When to leave the recipe

  • You want the 303's couplings — slides that bloom, accents that squelch. That's the acid recipe; this scaffold trades the couplings for a filter and envelopes you choose.
  • You want generative movement. This sequencer is deliberately deterministic; probability and ratchets are future emitters, and randomness belongs to objects that own a seed.
  • The line wants to be a song. Sixty-four steps is the ceiling; past that you're composing, and a piano roll is kinder than sixty-four step messages.

The robot on the radio

The vocoder chapter closes on "the casting is everything," and this recipe is the casting call. The sound has a documented pedigree — Bell Labs speech-compression research became, in musicians' hands, Kraftwerk's robot choirs and ELO's talking skies, and the machine has never left the radio since — but the records disagree on gear and agree on craft: a bright, busy carrier, an articulate modulator, and somebody enunciating like they mean it. All three are patching decisions.

Everything structural below is pinned by the kernel's tests and explained in the vocoder chapter: 24 bands, 50 Hz–12 kHz, everything you hear is carrier.

The carrier, built properly

The eternal failure is a dull carrier — high bands with nothing in them, consonants gone. Build it in three layers:

tap.vco~ (saw, f)        ──┐
tap.vco~ (saw, f, +7 c)  ──┼─▶ +~ ──▶ tap.vocoder~ right inlet
tap.noise~ (white) ─ *~ 0.1┘
  • Two saws, a few cents apart (@shape 2, detune ±4–7): harmonics to the top of the range, and the beating keeps long vowels alive. Use the Moog recipe's stack values; skip the octave-down voice — vocoded speech reads clearest with the energy above the fundamental.
  • The s and t budget. @sibilance 0.3 is the built-in version — a seeded noise source in the top bands' carrier, gated by the modulator's high-band envelopes, arriving exactly when consonants do. The manual alternative (a tenth of tap.noise~ summed into the carrier) remains the craftier option when you want to choose the noise color yourself.
  • Pitch is the performance. The vocoder never changes the carrier's pitch, so the carrier's notes are the melody. Held chords (an mc. stack of carriers) make the robot a choir; a single line makes it a lead vocalist.

The modulator, cast against type

Articulation beats fidelity — the chapter's measured point is that band envelopes carry everything, so contrast between bands is what you feed it. A cheap dynamic mic is fine; compression helps; and over-enunciating helps more than any knob. Keep the modulator out of the mix — the machine uses it, nobody should hear it.

The three settings

patchqresponse_intervalthe craft
the talking synth20 (default)30speak in rhythm; consonants land like drum hits
the choir10–15250sing sustained vowels; the carrier chord is the harmony
rhythm transfer25–4010–20drum loop as modulator; any sustained pad as carrier

q trades crispness against smoothness (narrow separates consonants, wide blends vowels); response_interval is the mouth's speed — attack and release in one knob. gain is linear makeup, and you will need some: a band-multiplied signal lands quieter than either input.

Two wiring facts that account for most dead patches: the modulator is the left inlet (a synth weakly filtered by your voice means the cables are backwards), and a silent carrier is silence no matter how loudly you speak — pinned by test, and the fastest debugging question in vocoding.

The songbook

The famous "vocoder songs" are the best syllabus for the craft — partly because several of them aren't vocoders, and knowing which is which teaches more than any preset. Provenance below follows the part's rule: documented where it's documented, labeled reconstruction where it isn't.

"In the Air Tonight" (1981) — the ghost choir

What's documented: Phil Collins ran the verse vocal through a Roland VP-330 — a soft vocoder, voiced like a string machine, mixed under a nearly whispered dry vocal. The reconstruction: this is the anti-robot patch. Carrier: two saws, detune ±4, imperfect 0.3, no noise layer — sibilance is what you don't want here — through tap.svf~ (@type lowpass @frequency 4000) to take the glass off. Vocoder: q 8–12, response_interval 120 — wide bands and a slow mouth blur the consonants into breath. Mix the vocoded return under the dry voice, a shadow rather than a double. The dry whisper carries the words; the vocoder carries the dread.

"Mr. Blue Sky" / "Mr. Roboto" / "Intergalactic" — the front-and-center robot

ELO (EMS vocoders, documented), Styx, and the Beastie Boys are the talking-synth patch played as a lead: bright carrier, crisp bands (q 20–30), fast mouth (response_interval 15–30), and the melody in the carrier's held notes while the words ride the modulator. Kraftwerk — the genre's founders, on custom and commercial hardware across the years — sit here too, usually with a single unison line rather than chords: the robot speaks in monophony. Enunciate. Then enunciate more.

One label to keep straight: Zapp, Roger Troutman, and the P-Funk talkbox records are not vocoders — a talkbox pipes the carrier into the performer's actual mouth and the room mic hears real articulation. Chasing that sound with this object gets you a cousin, not the thing.

"Hide and Seek" (2005) — the one that isn't a vocoder

What's documented: Imogen Heap sang into a harmonizer (the DigiTech Vocalist lineage), a keyboard choosing the chord — so every sound on the record is her actual voice, pitch-shifted into harmony, breath and formants intact. That's why it doesn't sound like a robot; there is no carrier. Three routes, honestly ranked:

  1. The right tool — tap.harmony~. This record's mechanism is exactly what the object does: formant-preserving voices holding a chord over the aligned dry voice. Its recipe — with the Bon Iver patches that extend the lineage — is A choir of one. (This object exists because this section's first draft had to work around its absence; the audit worked.)
  2. The manual fallback — a shifter stack. Voice into parallel tap.shift~ objects at chord intervals (tap.semitone2ratio feeds their ratio inlets). Keep the voicing within ±7 st — plain granular shifting moves formants with the pitch, and wide intervals go chipmunk where the formant-corrected routes don't.
  3. The vibe — the choir patch above. Speak-sing into the choir row's settings with an mc. carrier holding the chords. It will sound like a vocoder doing Imogen Heap, which is its own valid sound — just don't mistake it for the record's mechanism.

The plugin-era default — an Orange-school carrier

The late-90s software vocoders (the Orange Vocoder the most loved of them) changed the default sound of the effect: where hardware vocoders leaned on whatever synth was nearby, the plugins shipped with a built-in, very bright virtual-analog carrier — so "the plugin sound" is really a carrier voicing: wide, glossy, present. One honest line first: that plugin is still a shipping commercial product, and the house provenance rule applies — nothing here reverse-engineers it. What follows is our bright VA carrier in that school, built from this package's own oscillator:

tap.vco~ (saw, f,   detune -6, seed 11) ──┐
tap.vco~ (saw, f,   detune +6, seed 22) ──┼─▶ +~ ─▶ tap.svf~ (highshelf) ─▶ carrier
tap.vco~ (saw, f+12, gain -6,  seed 33) ──┤
tap.noise~ (white) ─ *~ 0.08 ─────────────┘

All three oscillators @shape 2 @imperfect 0.2 @jitter 2; the octave-up voice adds the gloss the era is remembered for; tap.svf~ @type highshelf @frequency 6000 @gain 4 is the sheen. Vocoder settings: q 25, response_interval 20. Play the carrier in fifths and octaves rather than full triads — the brightness supplies the width, and triads in a bright carrier smear the consonant bands.

When to leave the recipe

  • You want tuned speech, not a played carrier — the corrector (tap.tune~) moves the voice itself; the vocoder wears the voice over something else. Different identity theft.
  • You want formant-shifted or gender-shifted voice — that's spectral surgery, not band gating; the corridor starts at tap.spectra~.
  • You want intelligibility above all. Twenty-four analog-style bands are a voice, not a spectrograph; if every syllable must survive, dry speech mixed under the vocoded double is the radio trick that always works.

A choir of one

This chapter exists because this book's own audit demanded it. The vocoder songbook had to label "Hide and Seek" honestly — a harmonizer, not a vocoder: pitch-shifted copies of the actual voice, formants intact, no carrier anywhere — and the package had no object for that mechanism. Now it does. tap.harmony~ holds up to four formant-preserving voices at intervals you set in semitones, over a dry path the kernel delays into alignment so chords land as chords. This recipe is how to sing through it, and its worked examples are the modern masters of the effect: Bon Iver.

The claims behind the object are measured in the executed verification notebook and pinned in the kernel's test battery (tests/harmonizer_test.cpp): across two octaves of voicings every interval lands within 0.04 cents of its equal-tempered target under the DspTap yin oracle; the dry path is sample-aligned with the voices to a 3.7×10⁻⁸ residual — why chords land as chords, not flams; a synthetic formant bump stays near home only when formant is on (band centroid 1058 → 1154 Hz on a +7 shift, versus 1439 riding the full ratio with it off); and interval glides walk the pitch through the middle instead of jumping. The engine per voice is the DspTap phase vocoder — the same peak-locked shifting and LPC formant machinery the pitch machine chapter derives.

The instrument

voice ──▶ tap.tune~ (@speed 0, key of the song) ──▶ tap.harmony~ ──▶ out

The corrector upstream is optional but it is the modern sound: hard-snap the lead first and every harmonizer voice inherits the quantized pitch, so the stack locks like a keyboard instrument instead of drifting like a choir. Skip it and the stack breathes with your intonation — older, warmer, more Crosby-Stills than Vernon.

Three controls do the character work:

  • formant on is the entire point: an octave-down voice stays you, bigger. Off is the chipmunk-chorus bend — useful, but it stops being a choir.
  • chord is the performance surface: chord -12 3 7 sets three intervals and silences the fourth voice in one message. Wire a Max chord-to-intervals mapping (played notes minus the sung root) and the keyboard chooses the harmony live — the rig the credits of the records below describe.
  • glide at the 10 ms default snaps chord changes; at 300–500 ms the stack slides between chords, which no group of human singers can do and is worth featuring, not hiding.

One honest number: latency is one FFT frame (fftsize, default 1024 samples ≈ 21 ms at 48 kHz), dry path included. For live monitoring that is audible as a slight remove — performers adapt in minutes, but mix the monitor wet so they hear the instrument, not the delay ghost.

"Woods" (2009) — the stacked chapel

What's documented: Justin Vernon built "Woods" from many overdubbed a cappella takes, each hard-tuned — a chapel of his own voice, later the foundation of Kanye West's "Lost in the World." The record's mechanism is overdubs, and the recipe respects that:

  • The live approximation: tap.tune~ @speed 0tap.harmony~ with chord 3 7 12, @dry 1, all through a long dark reverb (the wash settings work). One pass, four-voice chapel.
  • The faithful version: record takes — sing each chord tone through the corrector alone, layer them, and use tap.harmony~ per take only to widen (chord 12 at @level1 0.4). Stacked takes decorrelate the way overdubs do; one harmonizer pass, however good, is one performance. The difference is the difference between a choir and a string patch.

"715 - CRΞΞKS" (2016) — the Messina

What's documented: the 22, A Million credits name "the Messina," the rig Chris Messina and Vernon built to pitch-stack his live voice into chords (the Prismizer-school effect associated with Francis and the Lights, and heard on Chance the Rapper's gospel records). "715 - CRΞΞKS" is that instrument a cappella: every sound is the processed voice.

voice ─▶ tap.tune~ (@speed 0) ─▶ tap.harmony~ @dry 1 @formant 1 @glide 10
                                     chord -12 3 7    (verse color)
                                     chord -12 4 7    (the lift)
                                     chord -5 3 10    (the ache)
  • @dry 1 — the lead lives inside the stack, equal citizen, exactly what makes the sound read as one multiplied person rather than lead-plus-backers.
  • Chords change per phrase, not per note: bind each chord list to a key or a pedal and play the harmony like slow organ stops.
  • The low voice carries the weight: -12 under a falsetto lead is the record's signature register trick. Keep it at full level; thin the upper voices (@level3 0.7) when the stack gets glassy.
  • No reverb, or almost none — the record's intimacy is the dry stack right against the microphone. Resist the wash this once.

The craft notes

  1. Feed it one voice. The formant model and the intervals both assume monophonic input — the kernel's header says so, and a strummed guitar through a "choir" proves it right within two bars.
  2. Mind the sum. Dry plus four unity voices is five voices; tap.limi~ or a *~ 0.5 after the object is the standing advice.
  3. Close voicings beat wide ones. ±12 is the working span; the engine's contract runs to ±24 and the top octave of that range is a sound effect, not a singer.
  4. The corrector's speed is the era dial. 0 ms is 2016; 40 ms is 1970s session stack; bypassed is a folk group.

When to leave the recipe

  • You want the robot. No carrier here, no bands — that's the vocoder, and the two chapters together are the voice-processing fork in the road: wear the voice over a synth, or multiply the voice itself.
  • You want real ensemble. Overdub real takes; the "Woods" section's faithful version is the honest ceiling of one person's choir.
  • You want harmony that follows chords you sing. The object holds intervals; it doesn't do music theory. The keyboard (or your patch's chord logic) is the brain — which is exactly how the famous rigs work.

The staircase and the wash

Shimmer has a documented birthplace: Brian Eno and Daniel Lanois in the early eighties, feeding a pitch shifter and a reverb into each other until a guitar came out sounding like weather. The pitchaccum chapter tells the half of the story that lives inside one object — the transposer-delay loop where every pass climbs again — and its recipes sketch the pairing. This recipe is the whole patch: the spiral, the wash, and the mix decisions that keep ten seconds of accumulated fifths from eating a track.

Measured claims are borrowed from the spiral staircase (the +7-becomes-+14 accumulation, the constant-sum grain envelopes, the 0.99 feedback cap) and borrowed rooms.

The chain

source ──▶ tap.pitchaccum~ ──▶ reverb (tap.verb~ or tap.convolve~) ──▶ return
   └────────────────── dry path ───────────────────────────────────────▶ out

Run it as a send: the source stays dry and full-size in the mix, and the shimmer return comes up underneath it like backlight. On the send, tap.pitchaccum~ at mix 100 (its own dry path stays home) and the reverb wet-only.

The spiral

controlsetting
trans1 / delay1 / fb1 / gain1+12 st / 400 ms / 75 / 50
trans2 / delay2 / fb2 / gain2+7 st / 650 ms / 60 / 50
xfade60 — smooth flanks, soft attacks
modfreq / moddepth / modphase0.3 Hz / 0.1 st / 90°
followoff for chords and pads; on for monophonic lines

The two shadows are doing different jobs: the octave climbs politely (+12, +24, +36 — always consonant), while the fifth rotates the harmony (+7, +14, +21 — a fifth, then a ninth, then a #11) and is where the Eno-school mystery comes from. Pull fb2 down toward 40 when the source is already harmonically rich; push fb1 toward 90 for the endless version — the loop is capped and DC-blocked, so "too long" is an aesthetic problem, not a stability one. The touch of modulation (moddepth 0.1, with modphase 90 breathing the shadows against each other) keeps a long spiral from sounding cloned — depth stays subtle or the climb turns seasick.

The wash

Either reverb works; they fail differently:

  • tap.verb~ (the designed tail): @mix 100 @decay 8 @damping 4000 @lowpass 8000 @delay 60 @modfreq 0.2 @moddepth 0.3. The damping matters more than the length — shimmer's accumulated highs need somewhere soft to land, and 4 kHz of loop damping is the difference between glow and glass dust.
  • tap.convolve~ (the borrowed room): a long church at @mix 100 @predelay 20, and pick the IR by its top end — audition the tail alone and reject anything that rings metallic up high, because the spiral will find it. (The field guide to rooms has the audition drill.)

Order matters and is worth an experiment: spiral → reverb (above) washes the staircase — the classic. Reverb → spiral transposes the wash itself and is wilder and less controllable; the historical chains did both, depending on the record.

Variants

  • The descent: trans1 -5, trans2 -12, long delays, feedback ~50 — the staircase into the basement. Darker damping (2–3 kHz); the low accumulation muddies fast, so shorter reverb.
  • The micro-halo: trans1 +0.15, trans2 -0.15, delays 60/90 ms, feedback ~50, xfade wide, modest reverb — no spiral at all, an expensive-sounding widener that flatters pads.
  • The gesture: store the halo in slot 1 and the full +12/+7 spiral in slot 2, then recall 2 8000 as the chorus lands — the morph engine glides every parameter, and the bloom is the production moment.

When to leave the recipe

  • The mix is dense. Shimmer is backlight; on a busy arrangement it reads as mud. It earns its keep on sparse sources — one guitar, one voice, one held pad.
  • You want rhythmic echoes climbing in pitch. The delays here serve the loop, not the grid; that patch is a tempo-synced delay into tap.shift~, built by hand.
  • You want the pitch to stay put. Then it's just reverb — go straight to the field guide.

Sixteenths into a listening filter

The envelope filter earned its place in funk on documented records — the Mu-Tron-era clavinets and basses of the seventies, Stevie Wonder's "Higher Ground" chief among them — and the autowah chapter is honest about what this object is instead: a model of the Snow White AutoWah, a different, throatier circuit. You are not summoning a Mu-Tron; you are plugging into a very good pedal that listens the same way. The funk is in what it listens to — which makes this the one recipe where the settings table is half the story and your right hand is the other half.

Measured behavior cited below (the sweep law exact to the design, the RC release, the 250 Hz → ~2.5 kHz hardware span) lives in the pedal that listens and its validation notebook.

How to think about the knobs

Two of them are calibration, one is the personality:

  • sensitivity matches the pedal to your source's level and your touch. Tune it so your normal hits open the filter halfway and your hard hits open it fully — the tanh knee compresses beyond that instead of slamming. Too high and everything pins; too low and the filter ignores you.
  • bias and range set where the sweep lives: resting frequency and octaves above it. The defaults (250 Hz, 3.3 octaves) are the hardware.
  • decay is the personality: how fast the filter falls back. Tens of ms is a wah articulation on every note — the funk setting. Hundreds is a swell that rides phrasing.

The patches

patchsettings
the clav chop@sensitivity 3 @attack 2 @decay 80 @bias 250 @range 3.3 @resonance 0.7 @mix 100
the bass quackrecall 2, then @decay 150 @resonance 0.6
the slow swellrecall 3, or @decay 900 @range 2.5 on pads
the cocked wahrecall 4sensitivity at −60 is the envelope off; park bias at 800–1200 Hz
  • The clav chop wants sixteenth-note playing with deliberate dynamic contrast — the filter turns your accents into vowels. mode 1 (bandpass, the circuit's other tap) is quackier and noticeably quieter; make it up with gain.
  • The bass quack starts from the factory bass voicing (slot 2 — lower bias, tighter range, the GB pedal's instrument switch as a preset). Fingers, not pick, and let notes ring — the release is a real RC discharge (measured: a pure exponential, σ = 0.004) and it sounds like circuitry when you leave it room.
  • The cocked wah is the secret mode: a fixed resonant filter with bias as a manual sweep — the parked-pedal midrange honk, and slot 4 ships it.
  • direction 1 sweeps down from bias — the extension the pedal never had; reverse-envelope funk on a clean chop is startling.

The two patch points nobody uses enough

  • The sidechain (right inlet): one sound wahs another. Kick → sidechain, pad → filter is the classic; a tap.808.seq~ row (through @pulse widened impulses) makes the filter sequenced while the pad sustains — an envelope filter with a drummer's timing.
  • The envelope outlet (right outlet, 0..1 signal): the detector as a free modulation source. Scale it into tap.vco~'s FM inlet, a tap.vca~ gain, or a second filter — one performance, many destinations. (In bypass the outlet goes to zero, so tap it from a live instance.)

When to leave the recipe

  • Your source has no dynamics. A static pad through an auto-wah is a static filter — feed the sidechain something rhythmic, or use tap.svf~ with an LFO and own the motion yourself.
  • You want the filter on a knob. That's the cocked wah until you want morphing responses — then tap.svf~'s morph is the tool.
  • You want the exact Mu-Tron quack. Raise resonance, try mode 1, and know the chapter's warning stands: you're modding a Snow White. The hardware A/B pass — the notebook cell waiting for the real pedal — will tell us precisely how far the model is from its own hardware, not from someone else's.

A field guide to rooms

The convolution chapter makes one promise that changes how you shop: tap.convolve~ is exact — measured to 10⁻¹² against direct convolution — so the engine contributes no character at all. Everything the effect sounds like is the impulse response you load. That turns "how do I get a good reverb?" into "how do I find, judge, and place a good room?" — a curation problem, and this recipe is the field guide.

Companion material: the convolution chapter and its verification notebook; every measured number cited below lives in one of them.

The shopping list

An IR is a recording of a space answering a click, and the internet holds decades of them — university acoustics archives, church-recording projects, hardware-unit captures released by their communities. What to bring home, by job:

  • A church or concert hall (2–5 s). The default "make it beautiful" space. Long tails flatter sustained, sparse material and drown busy mixes — the classic trade.
  • A plate. Not a room at all — a steel sheet's dense, fast-building wash. The vocal reverb of half the records you know; sits in a mix better than any hall because it has no early-reflection "walls" to argue with the stereo image.
  • A spring. The lo-fi twang of amp reverb; gloriously wrong on drums.
  • A small real room (0.3–0.8 s). The most useful and least glamorous purchase: drums and guitars recorded dry come alive with a believable space that reads as "a room," not "an effect."
  • Not a room. The chapter's point stands in practice: any filter you can record is loadable. A guitar body IR makes a piezo pickup sound like wood; a vowel is a formant filter; a single click is a delay.

Prefer 4-channel captures when offered: the engine runs the full true-stereo matrix (LL/LR/RL/RR), and the cross-feed paths are where "being in the room" lives — measured in the notebook at exact path gains with zero leakage. A 2-channel IR runs as dual mono (no cross-feed); a mono IR is the same room on both sides.

Judging a room in sixty seconds

Load it into the buffer~, then:

  1. Send a click through and listen to the tail alone (mix fully wet). You are auditioning the IR itself — the engine adds nothing. A good tail decays smoothly darker; a flutter or a metallic ring here will be on everything you send.
  2. Check the onset. Silence before the direct sound is pre-delay baked into the capture — trim it in an editor or accept it, but know it's there, because it stacks with the predelay you set and the engine's own blocksize samples of latency.
  3. A/B at matched loudness. normalize 1 is on by default and is energy-based, so a quiet cathedral capture and a hot plate land at comparable levels — judge the room, not the gain staging.

Placing the room in a patch

  • Send, don't insert. One tap.convolve~ fed by a send bus serves the whole patch, glues sources into one space, and keeps the option of riding the send. Keep mix 100 (wet-only) on a send; use mix as an insert dry/wet only on a single source.
  • predelay before you EQ. 10–30 ms separates the dry attack from the wash and buys clarity for free — the chapter's advice, and the first knob to reach for when a reverb "swallows" a vocal.
  • blocksize by role. Live input through the reverb: 64–128 (1.3–2.7 ms at 48 kHz — the measured cost is exactly blocksize samples). Mix-bus send: 512–2048, the CPU-cheap end, where the latency reads as a little extra pre-delay you set once and forget.
  • Swap rooms as a performance move. IR swaps are atomic and click-free (measured RMS across the swap: 21.9 → 22.1) — load verse-room and chorus-hall into two buffer~ objects and rebind with set <buffer-name> on the downbeat. (The buffer is the only way in: the object binds the buffer~ named by its first argument, and re-loading a file into that buffer re-transforms the IR automatically.)

When to leave the recipe

  • You want to design the tail — decay and damping knobs, modulation, gated endings. A static IR can't; tap.verb~ is the algorithmic sibling built for exactly that.
  • You want shimmer. The wash is only half of it — the spiral half is tap.pitchaccum~, and that pairing has its own recipe in this part.
  • You want zero latency. blocksize samples, full stop; at 64 that's small, not zero.

Chords with no keyboard

The comb-bank chapter ends on "strings, chords, drones, and gestures; no guitar required" — this recipe supplies the chords. tap.5comb~'s five voices tune in Hz (freq1..5), which means voicings are numbers you can keep, trade, and morph between; below is a small book of them, plus the excitation and morph craft that turns a filter bank into an instrument.

The mechanics cited here — ring time on a log map (20 ms–100 s), Hermite tuning, warp's stretched partials, phase's midpoint pluck — are measured and explained in five strings, no guitar and its machine chapter.

Voicings to keep

Tunings in Hz; MIDI equivalents in parentheses for orientation. The notes message tunes a voicing in one gesture — up to five MIDI note numbers, fractional allowed, so just-intonation intervals land exactly (notes 45 52 57 60.86 64 is the major glow with its true 5/4 third at 275 Hz) — and the Hz attributes remain for exact ratios like the bell plate.

voicingfreq1..5character
the factory chord80 / 120 / 160 / 200 / 102the legacy preset: a root-fifth-octave stack with a rub (102 against 80)
the open fifth55 / 110 / 165 / 220 / 330 (A1, A2, E3, A3, E4)power-chord drone; nothing to clash with any source
the major glow110 / 165 / 220 / 275 / 330 (A2, E3, A3, ~C#4, E4)just-intonation major: 275 is a pure 5/4 third, warmer than 12-TET's 277.2
the dark cluster65.4 / 77.8 / 98 / 130.8 / 196 (C2, D#2, G2, C3, G3)minor with a low rub; film-cue territory
the bell plate210 / 297 / 420 / 594 / 841non-octave (√2 ratios): inharmonic, gong-ish before warp even arrives

Masters make voicings performable: freq (0..2) transposes the whole bank proportionally — chords stay chords under the glide — and res/lp scale ring and brightness bank-wide.

Ringing them

  • Drone: res1..5 85, lp toward 5000. Feed it anything quiet and sustained — pink noise at low level, a field recording, your room tone. At res ≈ 100 the bank sustains essentially forever; the input stops being audio and becomes bowing pressure.
  • Pluck: res1..5 around 60–70 and excite with clicks or a sparse tap.808.rim~ (@model claves) pattern — every tick strums the chord. Drums work; speech works eerily well (the chapter's "resonator chord").
  • Strings, stiffened: warp 40 stretches the upper partials sharp — piano-ish, then bell-ish — while the compensated main tap keeps the pitch put. Pair with lp near 3000 for the felt-hammer version.
  • The midpoint pluck: phase 100 cancels the even harmonics — the hollow, clarinet-adjacent voicing of a string plucked exactly at its middle. On the bell plate tuning it turns purely ceremonial.

Watch the sum: five ringing combs stack like five strings. Ride gain down as res goes up, and tap.limi~ on the output is cheap insurance for the res 100 lifestyle.

The gesture

The bank's real instrument is the morph engine. Store the major glow in slot 1 and the dark cluster in slot 2 — then recall 2 8000 and every frequency, ring time, and damping glides for eight seconds through tunings you never chose, Hermite interpolation keeping the sweep continuous instead of zippered. The chapter's advice stands: automate nothing else. One long morph over a static source is a complete piece of sound design; grabbing a single fader mid-morph overrides just that parameter, which is the escape hatch when the in-between territory finds something worth keeping.

When to leave the recipe

  • One resonance, surgically placed: tap.comb~ is the single unit, or tap.svf~ @type bell when you want EQ, not a string.
  • You want echoes. Combs long enough to hear as repeats are delays wearing a costume — tap.delay~/tap.multitap~ are the honest tools.
  • You want more than five strings. The count is fixed; mc. wrapping the whole bank gives you choirs of banks, at which point you are building a sympathetic-string instrument and should budget CPU like it.