Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ same either way.
| `include/taptools/ladder.h` | `tap.ladder~` | ZDF Moog-style ladder (`tap::tools::ladder`) |
| `include/taptools/svf.h` | `tap.svf~` | Simper/Cytomic morphing SVF (`tap::tools::svf`) |
| `include/taptools/vco.h` | `tap.vco~` | Virtual-analog oscillator (`tap::tools::vco`) |
| `include/taptools/overdrive.h` | `tap.overdrive~` | LGW-voiced feedback overdrive (`tap::tools::od`) |
| `include/taptools/grm_comb.h` | `tap.5comb~` | GRM comb-bank recreation (`tap::tools::fivecomb`) |
| `include/taptools/grm_pitchaccum.h` | `tap.pitchaccum~` | GRM PitchAccum recreation (`tap::tools::pitchaccum`) |
| `include/taptools/conv_engine.h` | `tap.convolve~` | Partitioned (UPOLS) true-stereo convolution (`tap::tools::conv_engine`) |
Expand All @@ -33,18 +34,20 @@ Plus, all Max-free:
- **`tools/render/`** — offline WAV renderers (`diode_render`, `tb303_render`, `ladder_render`, `vco_render`,
`grm_comb_render`, `grm_pitchaccum_render`, `autowah_render`) for listening checks outside Max.
- **`tools/capi/`** — a small C ABI (`taptools_capi`) over the kernels for non-C++ consumers:
`conv_engine`, `svf`, `ladder`, `diode`, `tb303`, `vco`, and `autowah`.
`conv_engine`, `svf`, `ladder`, `diode`, `tb303`, `vco`, `autowah`, and `overdrive`.
- **`notebooks/`** — Jupyter verification notebooks driving the *actual shipping DSP* through the
C ABI via ctypes (`taptools_py.py`): `convolution_reverb.ipynb`, `svf.ipynb`, `ladder.ipynb`,
`vco.ipynb`, `tb303.ipynb` (the tap.303/tap.diode verification + the phase-2 WDF go/no-go evidence), and `autowah_validation.ipynb` (the hardware-calibration harness for the Snow
`vco.ipynb`, `tb303.ipynb` (the tap.303/tap.diode verification + the phase-2 WDF go/no-go evidence), `overdrive.ipynb` (the feedback-loop tilt, asymmetry harmonics, alias floor, and
body voicing, measured), and `autowah_validation.ipynb` (the hardware-calibration harness for the Snow
White model — its last section ingests reamped recordings of the real pedal).
- **`bench/`** — CPU benchmarks and the per-machine regression ratchet (see `bench/README.md`).
- **`book/`** — *Tools on Tap*, the mdBook field guide (the AmbiTap/SampleRateTap/MuTap book
pattern): one chapter per object family, every claim measured by the notebooks/tests. Built
and published to Pages by `.github/workflows/docs.yml`. Ten user-facing chapters across four
and published to Pages by `.github/workflows/docs.yml`. Fourteen user-facing chapters across seven
parts — sources (`vco`), filters (`svf`, `ladder`, `autowah`), strings/rooms/spirals
(`convolve`, `5comb`, `pitchaccum`), the spectral set (`vocoder`, `nr`, `spectra`) — plus
**Part V, "The machine, file by file"**: one deep-dive appendix per kernel header
(`convolve`, `5comb`, `pitchaccum`), the spectral set (`vocoder`, `nr`, `spectra`), the rhythm
section (`acid`, `drums`), staying in tune (`tune`), the pedalboard (`overdrive`) — plus
**"The machine, file by file"**: one deep-dive appendix per kernel header
(SampleRateTap-style) deriving the math, reviewing the code, and recording why each
algorithm is built the way it is.

Expand Down
7 changes: 6 additions & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@

- [The note you meant](tune.md)

# Part VII — The machine, file by file
# Part VII — The pedalboard

- [Distortion with a memory](overdrive.md)

# Part VIII — The machine, file by file

- [Solving the filter on paper: svf.h](machine/svf.md)
- [The nonlinear loop: ladder.h](machine/ladder.md)
Expand All @@ -50,3 +54,4 @@
- [Time as a function of phase: step_seq.h](machine/seq.md)
- [Three ways to move a pitch: yin.h, psola.h, pvoc.h](machine/pitch.md)
- [The nearest allowed note: tune.h](machine/tune.md)
- [The clipper in the loop: overdrive.h](machine/overdrive.md)
9 changes: 7 additions & 2 deletions book/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ The book is organized the way a patch is:
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 — The machine, file by file**: the SampleRateTap-style deep dives —
- **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–IV are for driving the objects; Part V is for trusting them —
all. Parts I–VII are for driving the objects; Part VIII is for trusting them —
or changing them.

More chapters land as objects mature; the utility and Jitter objects live in
Expand Down
170 changes: 170 additions & 0 deletions book/src/machine/overdrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# 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:

```text
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)
```

`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

```text
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:

```text
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:

```text
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.

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.

## 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),
and every number is a cell in
[the verification notebook](https://github.com/tap/TapTools/blob/main/notebooks/overdrive.ipynb).
103 changes: 103 additions & 0 deletions book/src/overdrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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](https://github.com/tap/TapTools/blob/main/notebooks/overdrive.ipynb),
where every number below is an executed, plotted measurement of the shipping
kernel.

## 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.

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.

## 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.

### `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.)

### `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](machine/overdrive.md).
Loading
Loading