Skip to content

The Radiohead family: nine kernels, twelve chapters, three recipes - #34

Merged
tap merged 22 commits into
mainfrom
claude/radiohead-taptools-sfx-is456q
Aug 18, 2026
Merged

The Radiohead family: nine kernels, twelve chapters, three recipes#34
tap merged 22 commits into
mainfrom
claude/radiohead-taptools-sfx-is456q

Conversation

@tap

@tap tap commented Aug 18, 2026

Copy link
Copy Markdown
Owner

What this changes

Adds the Radiohead family — nine objects across seven new kernel headers — with their tests, C ABI, executed notebooks, render scenes, book chapters and recipes. Also fixes fuzz.h's long-open oversampler question, and retracts a measurement this repo had recorded against tap.pitchaccum~ (now #33).

kernel objects
tapecho.h tap.tapecho~
stammer.h tap.stammer~
fuzz.h tap.fuzz~
scrub.h tap.scrub~
touche.h tap.touche~
diffuseur.h tap.metallique~, tap.palme~
ondes.h tap.triode~, tap.ondes~

Why

The family's thesis is the control is the instrument — objects you ride rather than set up and walk away from — which is what earns them a part of their own in the book rather than being filed by topology next to tap.discreet~.

Two of them exist because reading the sources changed the plan. The family plan assumed the ondes Martenot was an oscillator with waveform registers; it is heterodyne, its oscillators measure at about 0.03 % second harmonic, and its largest single source of harmonics is the demodulator's 2|cos| envelope — 14.0 dB down at H2 — before any valve. The plan's instruction to "synthesize the difference tone directly as a sinusoid" would have thrown that away. And the diffuseurs are driven, not struck, so garden.h's modal maths carries over while its strike envelopes do not.

The fuzz.h fix is the payoff from a cross-file observation: ondes.h runs the same 8th-order filters around a comparably hard nonlinearity but as a source with nothing zero-stuffed, and its oversampling sequence never reversed. That pointed at imaging as the culprit in the fuzz, and acting on it confirmed it.

Verification

Built and run here:

  • ctest322 test cases, 1 758 389 assertions, all passing. New: tapecho_test.cpp, stammer_test.cpp, fuzz_test.cpp, touche_test.cpp (11 scenarios), scrub_test.cpp (13), diffuseur_test.cpp (18), ondes_test.cpp (18).
  • clang-format-18 --dry-run --Werror over the same file set CI checks: clean.
  • All notebooks re-executed against a freshly built taptools_capi, zero errors and zero warnings.
  • mdbook build: clean, and every internal link and image path in src/ resolves (checked with a script).
  • radiohead_render built and all 19 scenes rendered and listened to.
  • book/figures/radiohead.py re-run end to end; the six pre-existing SVGs came back byte-equivalent modulo timestamps, so only the eight new ones are in the diff.

Load-bearing measurements, each pinned twice (executed notebook cell + Catch2 scenario):

  • tapecho.h — head returns land on span × ratio; bitwise equality with delay.h's multitap when the tape path is neutral; regeneration past unity self-oscillates but stays under |in|max + regen/drive; wow 10.91 cents measured against 10.88 predicted.
  • stammer.h — pinned-dice one-step-delay identity, bitwise; occupancy 41 / 76 / 90 / 96 % across four density/repeat pairs; different seeds change 89.4 % of samples; slice similarity 1.000 (sine) against 0.286 (played phrase).
  • scrub.h — held still at unity pitch the object is the input delayed, to 4.4e-16; transposing retains 98.8 % of a clean shifter's band energy (worst 91.7 %) at 92.0 % of its concentration (worst 75.0 %).
  • diffuseur.h — a cabinet is bitwise transducer → body, and the reverse wiring differs by 28 % of peak; plate weights sum to exactly 1 with unit peak gain per mode; all twelve palme strings ≥ 4.4× selective.
  • ondes.h — the closed-form envelope matches a full 80 kHz heterodyne-plus-diode-plus-RC simulation to within 0.10 dB on every harmonic (uniform 3.0–3.2 % level offset); H2 runs −14.0 dB at A2 to −19.3 dB at A6; drive sweeps THD 0.221 → 0.344 monotonically.
  • touche.h — the published Quartier et al. table interpolated, not fitted: 50 dB from 4.3 mm to 8.8 mm, exact silence below.
  • fuzz.h — the new cascade's worst step-up past 2× is a ratio of 1.017 (was up to 3× worse per doubling); 4× and 8× improved by two to four orders of magnitude; 3.16 % of a core at 8× against 3.02 % before.

Not done here, stated plainly:

  • No in-Max runtime validation. None of the nine has been loaded in a licensed Max, and no *.maxtest.maxpat starters exist for them. That gate lives in TapTools-Max and needs a Mac; it is the one place where "tested" is genuinely incomplete for this work.
  • clang-tidy is not a gate in this repo (its style workflow is drift-check + clang-format). The externals' tidy pass runs in TapTools-Max and is green there on the companion branch.
  • CI is the first real gate for the macOS/Windows halves of the build matrix.

Notes for the reviewer

  • Contract change: fuzz.h's oversample default moves 2 → 4. Two separate errors made the old default wrong. The chain zero-stuffed by N in one step and filtered once at 0.45/N (0.056 normalized at 8×), which left N−1 images for one filter and made more oversampling measure worse; it now cascades one 2× stage per doubling at a corner that never tightens. And every number behind the old default came from a single test tone at 3733 Hz, where 2× happens to look best — swept across nine tones, 2× collapses above about 6 kHz and at 10499 Hz measures worse than no oversampling at all. No released consumer is affected: tap.fuzz~ has only ever existed on this branch.

  • Submodule pin. No pin moves in this repo. The consumer PR in TapTools-Max moves submodules/taptools to this branch's tip — after this merges by rebase/squash, that pin needs repointing at the identical tree on main so it stays reachable once the branch is deleted.

  • Notebooks re-executed, because behaviour changed. They are committed executed.

  • A retraction rides along, and it is the most reviewable thing here. This repo previously recorded a near-total cancellation in tap.pitchaccum~ (band energy 0.004 at 311 Hz +19 semitones). It does not survive rechecking: the sweep integrated a fixed ±15 Hz band, about 115 cents wide at 220 Hz but only 26 cents at 932 Hz, so at high transposed pitches the probe was narrower than the shifter's own spread. Widened to a constant 3 %, those points read 0.63 and 0.85. What survives is filed as tap.pitchaccum~: the shifted line sits beside the intended pitch, not on it #33 and is smaller and honestly ambiguous. src/machine/scrub.md now carries the second half of its own rule — a band wide enough in the units the process works in — because the mistake was committed one section after writing the warning against it.

  • A defect found by a wrapper test, not a kernel test. voice::set_smooth_ms never forwarded to touche::key, so an ondes key at rest kept sounding for 20 ms. The kernel suite tests DSP promises; the wrapper suite tests what a patcher observes, and those are not the same set. Fixed here with its own scenario.

  • Provenance, since most of this is recreation rather than port. Fletcher & Rossing for the plate and string physics; Roads for the granular tradition; Yeh/Abel/Smith DAFx-07 for both distortion cascades; Koren 1996 with Cohen & Hélie 2010 for the tube, with parameters transcribed from Najnudel et al. 2020 Table II; Quartier et al. 2015 for the intensity key; Wijnand et al. 2023 for the diffuseurs. Where a source stops, the header says so rather than filling the gap: the diffuseur bodies are labelled recreations of the general physics because no ondes-specific modal measurement exists in any source obtained, and the ondes waveform registers are deliberately absent — a second source hunt is recorded in book/PLAN-ondes.md, including the two leads worth trying next (Leipp's Bulletin du GAM n°60, and Martenot's patents).

  • Book. Six user-facing chapters and six appendices across Parts V and X, plus three recipes in Part XI. introduction.md's part list is a second copy of SUMMARY's structure with nothing checking them against each other, so it was updated by hand — worth knowing for the next part insertion.


Generated by Claude Code

claude and others added 22 commits August 15, 2026 21:27
Drafting record for a family of performed-electronics kernels: the
multi-head tape echo, the live stutter rig, the Ondes Martenot and its
diffuseurs, the ShredMaster-school fuzz, and the Kaoss-school scrub.
Surveyed 2026-08-15 and amended the same day against the Eno components
wave before any code: the echo becomes composition over tape_loop.h, the
diffuseurs inherit the garden's modal idiom, and the family adopts the
components-first delivery template, the seeded-randomness convention,
and the Bloom -> garden trademark posture from the start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The first kernel of the Radiohead family, and the plan's prediction tested:
tapecho.h is composition over tape_loop.h, not a new machine. One reel in
delay-line topology, one transport, one wear stage, and up to four playback
heads at settable positions along the path -- span_ms is the motor (the delay
of a ratio-1.0 head) so a speed change moves every head together, as varispeed
does.

Where discreet.h lets regeneration reach exactly 1.0 because the wear path is
the stabilizer, this one goes past unity into deliberate sound-on-sound howl,
bounded by the saturator's 1/drive ceiling rather than a feedback cap -- and
capped back to 1.0 per sample whenever drive is 0, since that is the only
thing holding the past-unity regime. The head layout is a nominal even
spacing, freely settable; no spacings are claimed as measured from any unit.

The null test is the load-bearing one: with the tape path neutral, a one-head
echo is bitwise delay.h's multitap, pinned in the kernel tests and again
across the C ABI in the notebook. Also measured: per-pass generation loss
within 0.2% of the analytic wear transfer, wow at 10.91 cents against 10.88
predicted, and every past-unity drive setting plateauing under its ceiling.

Ships the layer the family template asks for: 11 Catch2 scenarios, the C ABI
plus ctypes surface (TapEcho), the executed tapecho.ipynb, and radiohead_render
with four performed scenarios. The Max wrapper and the book chapter are still
to come.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The wrapper, its min-api scenarios, the reference page and the help patcher
landed in TapTools-Max alongside the pin bump, so the plan's per-object
status now reads shipped end-to-end with only the book chapter and the
on-Mac validation pass outstanding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The second Radiohead-family kernel, and an original design rather than a
port: capture the input continuously, then on a rhythmic grid roll dice and
re-fire a slice of what just went past. Brassage tradition (Roads,
Microsound); the lineage to the band's own Max rigs informs what the object
is for and nothing about what the code does.

The planned component split holds: a capture (a tape_loop.h reel with one
advancing head) and a slicer that owns every draw and the slice in flight,
under a thin machine. Like tapecho.h's head, a slicer needs a capture, so it
is documented as a component for composition and testing rather than
standalone-external material.

The suite and the notebook both lean on a pinned-dice identity: with density
1, whole-step slices, one forward pass and no flank, the machine must reduce
to exactly a one-step delay, bitwise. That single identity pins the grid
countdown, the slice origin and the playback head together -- much stronger
than chasing three off-by-ones separately. Around it: the seeded-performance
contract (same seed bit-identical, a different seed changes 89% of samples),
the garden.h disabled-generator contract (at density 0 the dice are never
rolled, so the seed provably cannot matter and the object is a bitwise
bypass at any mix), the repeat invariant, the reverse identity, and the
flanks' exact edges.

The material contract is measured at its premise rather than asserted:
slices of a sustained sine are 1.000 alike by magnitude spectrum, slices of
a plucked phrase 0.286 -- re-ordering interchangeable things cannot do much,
which is why this object wants transients.

Ships the family template: 9 Catch2 scenarios, the C ABI plus ctypes surface
(Stammer), the executed stammer.ipynb, and three radiohead_render scenarios
including the performed disintegration. The Max wrapper and the chapter are
still to come.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The wrapper, its min-api scenarios, the reference page and the help patcher
landed in TapTools-Max alongside the pin bump, so two of the family's five
objects now read shipped end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Four chapters in a new Part V, "The machines you ride" -- the name the
family thesis earned: where the Eno part is systems you set up and walk away
from, these are objects whose point is the performance surface.

User-facing: "Four heads and a motor" (tap.tapecho~) opens on that posture
difference and treats the bitwise null test against delay.h as the design
claim rather than a curiosity; "The part that comes apart" (tap.stammer~)
leads with the thing to internalize before patching -- density grabs,
repeats holds -- and presents the material contract as a measurement.

Machine appendices: tapecho.h is deliberately the book's shortest, because
its content is that tape_loop.h needed no changes at all to serve a second
topology; stammer.h spends its space on how to pin three interacting integer
clocks with one identity, plus the draw order as contract and why not every
class boundary is a seam.

Figures are measured through the C ABI per the eno.py rule. The occupancy
figure's first draft emitted a 117 MB SVG from a per-sample fill_between at
48 kHz; busy stretches are now drawn as spans computed from run boundaries,
same picture at 44 KB.

Also fixes introduction.md, whose part list had been stale since the Eno
wave landed -- it still called Part IV "The spectral set" and stopped at
Part IX. It now matches the eleven parts that exist. The drafting record
notes that the list is an unchecked second copy of SUMMARY's structure.

mdbook builds clean with create-missing = false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The sources exist and are identified, with an honest column for how far each
was actually read: the Acta Acustica intensity-key study, the Forum
Acusticum diffuseur-loudspeaker paper (read in full), the IEEE circuit
simulation and its ribbon-oscillator companion, plus the Leipp and
Laurendeau primary references they cite.

Two design assumptions were corrected by reading rather than by assuming.
The touche d'intensite maps displacement, not force, across a 50 dB range
per note -- so the control input is a position and only the taper is still
open. And the diffuseurs are driven, not struck: the metallique is a
motor-excited gong and the palme an electromagnet driving twelve strings, so
the garden's strike-excited modal idiom applies for the resonator maths but
not for the excitation. The early transducer is also inherently nonlinear,
which a resonator-only model would miss entirely.

Also records that hobbyist build pages give the palme 24 strings where the
peer-reviewed source says 12, and that those pages are unciteable here.

The gate is clearer but not open: three full texts are still needed and
automated retrieval is blocked, which is a manual step rather than something
to guess around.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The OK Computer-era dirt, and a sibling of overdrive.h rather than a
replacement: that object is a feedback soft-clipper chasing the TS lineage,
this is the harder, more scooped school -- two cascaded clipping stages and a
bass/contrast/treble voicing section.

The method is Yeh, Abel & Smith's DAFx-07 simplified cascade (conditioning
filter -> memoryless nonlinearity -> equalization filter, twice), which
supplies the architecture, the justification for approximating the diode
limiter's moving-pole ODE with a static curve between fixed filters, the
curve family, and the reason asymmetry exists. It is a recreation of a
circuit class: no component value or corner is claimed as measured from any
unit, and the voicing constants are the sound of the object.

Two defects the measurements caught. The first cut had the second stage
fully clipped at gain 0 -- the knob did nothing over most of its travel --
because the tanh family's small-signal slope compounds across a cascade;
retuned, the harmonic ratio now sweeps 0.010 to 0.358. And the house
4th-order oversampling filter made aliasing *worse* at 4x and 8x than at 2x,
so this kernel uses 8th order, which restores the monotone improvement the
setting promises.

Two test-design errors are recorded in the suite because both are easy to
repeat: an alias test whose tone divided the sample rate, so every fold
landed on a harmonic and was invisible; and probe frequencies near enough
the fundamental to measure window leakage instead. The test now asserts only
what is true -- two orders of magnitude against no oversampling, and
deliberately no ordering among 2x/4x/8x, where the residual is -60 dB and
noise dominates.

Nine Catch2 scenarios plus the C ABI and ctypes surface. The notebook, a
render scenario, the Max slice and the chapter are still to come.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Adds the executed fuzz.ipynb and three radiohead_render scenarios (the gain
sweep, the voicing section, and the knee sharpening into the even
harmonics).

Building the notebook caught an overclaim in the kernel header, the plan and
the previous commit message: 8th-order anti-aliasing was described as
restoring the monotone improvement that oversampling promises. It does not.
Measured against a 3733 Hz tone, fold energy runs 1.2e-1 / 2.7e-5 / 7.4e-4 /
1.8e-3 at 1x / 2x / 4x / 8x -- every factor is worth having over none, and 2x
is the best of them, so 2x is now the default rather than 4x.

Eighth order is still the right filter (it improves 4x about sixfold over the
house 4th-order pattern); it just does not do what was claimed. The cause of
the non-monotonicity is recorded as open, with one hypothesis tested and
ruled out -- biquads going ill-conditioned at the low normalized cutoffs a
high factor needs, disproved by an impulse-response check showing clean decay
to denormal at every factor -- and the untested one (imaging from zero-stuff
upsampling intermodulating in the clipper) named along with the fix it would
imply, cascaded 2x resampling.

The kernel test already asserted only what was true here, so it needed no
change: all factors beat 1x by orders of magnitude, and no ordering among
them is pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The full texts arrived, so the source hunt closes. The touche d'intensite is
now fully specified and its table is reproduced in the plan, because the
table is the specification: 4.5 mm of travel carries the whole 50 dB, the map
is memoryless (the paper shows it does not depend on gesture velocity), the
taper is distinctly not linear-in-dB against displacement, it is linear in dB
against log force over the playable region, and it separates cleanly from
pitch.

The bigger news is an amendment. The plan assumed a waveform-register VCO
built on vco.h. The circuit paper models five coupled stages and finds the
oscillators are essentially pure -- about 0.03% second-harmonic distortion --
which is the simplification the authors themselves use to reach real time.
The timbre comes from the two triode stages after the demodulator and then
from the diffuseur. So the kernel is a clean heterodyne source into a
triode-flavoured nonlinearity into the touche curve into the diffuseurs,
which is nearer overdrive.h and fuzz.h than vco.h, and a different object
than the one sketched. Their full solve runs at 768 kHz and their plugin
eats 85% of a laptop core, so the documented simplifications are the route,
not a faithful circuit solve.

The gate is open in the sense that matters: what remains is design, not
sourcing. tap.ondes~ needs a design pass against these findings first.

Also records that the papers are open access but sit behind an
anti-automation wall with no mirror, and that the wall was not circumvented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
"The dirt with two stages" places tap.fuzz~ against tap.overdrive~ -- two
dirt objects, not competitors -- and spends its length on the three things a
patcher can act on: the knee as a character control, why the gain floor sits
below unity, and why oversample 2 beats 8.

The machine appendix is deliberately about two mistakes, because the DSP is
a published recipe followed closely and the failures are the reusable part.
Small-signal gain compounds across a cascade, which left the second stage
saturated at gain 0 and was inaudible precisely because the object sounded
like a distortion at every setting. And the house oversampling filter
measured wrong here -- but so did my first explanation for it, so the
appendix records the ruled-out hypothesis (biquad conditioning, disproved by
an impulse-response check) next to the surviving one rather than leaving a
vague note. The two aliasing test-design errors are written up too, since
both passed review the first time.

Two measured figures via book/figures/radiohead.py, SUMMARY and the
introduction's part list updated, and the chapters' drafting record extended.
mdbook builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The papers describe a different object than the family plan sketched, so
this is the design pass they forced, written before any code.

The instrument is heterodyne and its oscillators are measured as essentially
pure, so the kernel is a clean source into a triode nonlinearity into the
intensity-key gain law into a driven resonator -- nearer fuzz.h and garden.h
than vco.h. Synthesizing the difference tone directly is a documented
simplification of a published model rather than a shortcut, and the header
should carry the number that justifies it: the full port-Hamiltonian solve
runs at 768 kHz and its plugin eats 85% of a laptop core.

The component split differs from the family's recent objects in a way worth
planning for: three of the four parts are independently useful. The touche is
a measured, published expressive gain curve that would improve almost any
object, and the diffuseurs run anything you feed them, so both should be
standalone externals from the start rather than seams discovered later.

Work is ordered to front-load those: touche first (fully specified, small,
shippable alone), then the diffuseurs, then the triode -- which needs a
listening comparison to settle whether to use a published grid-conduction
curve or the house tanh family -- and the source last.

Records what is still unsourced: diffuseur-specific modal data, and the
waveform-register filter shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The first piece of the Ondes Martenot, and the one that is useful on its
own: the touche d'intensite as a gain law. The curve is not modelled or
fitted -- it is Quartier et al.'s published measurement (Acta Acustica
101(2), 2015, Table II), interpolated with monotone cubic segments through
all seven points. 50 dB over 4.5 mm, referenced to 0 dB at full press, and
the seven points come back to within 6e-5 dB.

Monotone cubic rather than a spline because an overshoot here is a
non-monotone gain -- audible as a dip while pressing harder. And
interpolation rather than a fit because the shape is the whole point: equal
dB steps correspond to displacement steps of 1.0, 0.6, 0.5, 0.4, 0.5, 1.5
mm, so a straight line in dB-against-mm departs by 8.3 dB and throws away
the property that makes the key expressive.

Two things the tests caught. The normalized domain has to be the physical
travel, not the measured band: mapping 0..1 onto 4.3-8.8 mm put position 0
exactly on the first published point while returning silence there. The
paper puts gestures at roughly 3-9.5 mm with the measured band inside, so
position spans 9.5 mm and the bottom 45% is genuinely silent -- the key
bending before it reaches the powder bag. And the dead zone belongs in the
lookup rather than the table, because zeroing entries below the floor put a
cliff next to it and a query landing on the floor read 4.2 dB low.

Eleven Catch2 scenarios, the C ABI plus ctypes surface, the executed
notebook, and a render putting the curve against a linear fade and a fade
linear in dB -- it is audibly neither.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Two objects in one pass, both from book/PLAN-radiohead-family.md.

diffuseur.h — tap.metallique~ and tap.palme~, the Ondes Martenot's resonating
loudspeakers as DRIVEN resonators. garden.h's modal maths carries over; its
strike envelopes do not. Five components under two thin cabinets: `mode` (the
constant-peak-gain two-pole resonator, unit peak gain at any Q and exact nulls
at DC and Nyquist, so the bank needs neither limiter nor blocker), `plate`
(eight free-circular-plate modes as beating doublets), `sympathetic` and `harp`
(twelve damped waveguide loops), and `transducer` (the moving-iron driver).

The signal order is a claim about the instrument and is pinned by a null test:
the cabinet is exactly transducer -> body, bitwise, and the reversed wiring
differs by 28% of peak. The transducer's bound is 2/saturation rather than the
1/saturation the saturator alone would give, because removing the DC from a
hard-driven squared law doubles the worst-case swing.

The bodies are recreations of the general physics (Fletcher & Rossing), stated
as such: no ondes-specific modal measurement exists in any of the sources. The
palme gets twelve strings per the peer-reviewed source, not the twenty-four of
the hobbyist build pages.

scrub.h — tap.scrub~, a granular scrub pad whose position and pitch are two
independent performable signals. The tape is stammer::capture itself, shared
rather than copied, as that header's limits promised; the only addition the
stutter needed was the fractional read its slices never used. Hann at hop
size/2 overlap-adds to exactly 1, so held still at unity pitch the scrub is the
input delayed, to 4.4e-16.

One real defect found by measurement and fixed: anchoring grains at the
position advances their origins at the write head's speed, so transposition
applied only inside a grain, the average read rate came back to 1, and a steady
tone emerged at its ORIGINAL pitch with a comb around it. The read head is now
phase-continuous, wrapped back only after wandering +-1.5 grains (a bound
chosen by sweep). A measurement warning is recorded with it: a single-bin probe
read the fixed kernel as broken, 0.02 where the band figure was 0.43.

Also here: 31 Catch2 scenarios across the two suites, the C ABI and ctypes
surfaces (including the bare `Plate` and `Transducer` components), two executed
notebooks, four radiohead_render scenarios, and the README's kernel tables
brought up to date with the six objects that had fallen out of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
…urce

ondes.h — the last two pieces of the instrument, and the two the family plan
had wrong. Three components under a thin composition: `triode` (one
common-cathode stage solved on its load line), `detector` (the heterodyne pair
and its envelope detector, in closed form), and `voice` (detector into two
triode stages into the intensity key).

Almost none of this is voiced by ear. Najnudel, Hélie, Roze & Boutin (IEEE/ACM
TASLP 28, 2020) name the tube model — the enhanced Norman Koren law — write out
its equations, and publish parameter sets fitted to the actual valves in ondes
No. 169, along with every stage's supply voltage and cathode resistor. So the
triode is a citation rather than a design: the static load-line solution at a
published operating point, which is a memoryless nonlinearity in the DAFx-07
sense, so tabulating it is not an approximation of the model but the model
itself. The fitted 6C5 lands at 8.85 mA against its datasheet's 8 mA typical.

The plan's central instruction for the source was wrong, and catching it is the
most valuable thing here. "Synthesize the difference tone directly as a
sinusoid" would have thrown away the instrument's largest source of harmonics:
the paper's licence to use a sinewave generator applies to the OSCILLATORS, and
the demodulator is an envelope detector, not a mixer. The envelope of two summed
oscillators is 2|cos|, whose series puts H2 at -14.0 dB and H3 at -21.3 dB
before any valve touches the signal.

What replaces the carrier is an identity rather than a simplification: the
envelope of cos(P) + d*cos(P-p) is exactly sqrt(1 + d^2 + 2d cos p), so the
80 kHz carrier drops out of the arithmetic. Running the published 200 us RC
detector on that closed form reproduces the full heterodyne-plus-diode-plus-RC
simulation to within 0.10 dB on every harmonic at every pitch, with one uniform
3% level offset. It also makes oscillator balance a real physical timbre
control, and the detector's pitch dependence falls out free.

Two errors found by measurement and fixed: a stage that quietly un-inverted
itself applied the tube's asymmetry to the wrong side of the waveform, so the
drive knob REDUCED harmonics as it was turned up; and an oversampling probe that
measured its own leakage rather than aliasing. Two things the sources do not
settle — where the intensity key sits in the chain, and the winding sense of the
transformer between the stages — are switches rather than silent guesses,
because both measure as audible.

Also: evidence for fuzz.h's open oversampler question. This object runs the same
8th-order chain around a comparably hard nonlinearity but as a SOURCE, with
nothing zero-stuffed and therefore no images, and its sequence never reverses
where fuzz.h's did. Recorded in both headers as evidence, not proof.

17 Catch2 scenarios, the C ABI and ctypes surfaces (Triode, Detector, Ondes,
plus the bare tube law), the executed notebooks/ondes.ipynb, and four render
scenarios including the whole instrument through each of its diffuseurs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
`voice::process()` drove the ribbon and the key through their setters, which
means the key's 20 ms anti-zipper ramp gets re-targeted on every sample when a
signal is driving it — so the key lags the hand by the full slew time instead of
tracking it. That is exactly wrong for the one control the instrument is played
with.

`process(semitones, key_position)` takes both straight from the caller with the
ramps bypassed, the way scrub.h and touche.h already do it; the ramps still tick
so switching back to the attribute path is continuous rather than a jump. The C
ABI's process_mod now routes through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
touche::key keeps its own anti-zipper slew, and voice::set_smooth_ms was not
reaching it — so the one control the instrument is actually played with slewed
at 20 ms whatever the caller asked for, and a key set to rest kept sounding for
that long. Found by a wrapper test in TapTools-Max asking for silence at a rest
position and getting 20 ms of sound; pinned here now so the kernel carries it.

prepare() re-applies it too, since touche::key::prepare resets its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Nine objects shipped over the last three days without chapters. They become
six: chapters follow instruments, not externals.

User-facing (Part V — The machines you ride):

- src/scrub.md — "Two hands on the same tape". Filed beside stammer.md
  because the two share a tape literally. Carries the delay identity
  (4.4e-16), the position/pitch decoupling, and the warble measured rather
  than apologized for (98.8% of a clean shifter's band energy, 92.0% of its
  focus), including the open question against tap.pitchaccum~.
- src/diffuseurs.md — "Loudspeakers you can play". Both diffuseurs in one
  chapter: driven not struck, the transducer upstream of the body (bitwise
  null; the reverse wiring differs by 28% of peak), twelve strings not
  twenty-four, and the line between the peer-reviewed instruments and the
  recreated bodies.
- src/ondes.md — "The instrument that is not a synthesizer". tap.ondes~,
  tap.triode~ and tap.touche~ together: heterodyne, the demodulator as the
  largest harmonic source (-14.0/-21.3/-26.4 dB), the closed-form envelope,
  the ribbon linear in semitones, 50 dB in 4.5 mm, the valve as a citation,
  and the two controls that are labelled choices.

Machine appendices (Part X), each earning its length from what went wrong:

- machine/scrub.md — grains anchored at the position cancel the
  transposition, and a single-bin probe reads the fix as the bug.
- machine/diffuseur.md — unit peak gain removing the limiter, the DC blocker
  and the decay/level coupling in one choice; the 2/saturation correction;
  the selectivity test that measured its own on/off step.
- machine/ondes.md — the stage that needed no design, the detector that is
  an identity, the sign error that ran the drive knob backwards, three
  measurements that lied, and the oversampling evidence for fuzz.h's open
  question.

Eight new measured figures in book/figures/radiohead.py, same contract as
the rest: the shipping kernels through the C ABI, never illustrations.
SUMMARY, the introduction's part list and the README's chapter count follow;
PLAN-radiohead-chapters.md carries the drafting record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
touche.h sorts before the tr808_* block under the house SortIncludes:
CaseSensitive, and it was appended after them. This has failed the
clang-format job on every commit since tap.touche~ landed; the local
pre-commit hook was not installed in this clone, which is why six commits
went out without it being caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The recorded "near-total cancellation" in tap.pitchaccum~ (band energy 0.004
at 311 Hz +19 semitones) was an artifact of the metric, not the object. The
sweep integrated a fixed +-15 Hz band: about 115 cents wide at 220 Hz but
only 26 cents at 932 Hz, so at high transposed pitches the probe was
narrower than the shifter's own spread and missed the energy. Widened to a
constant 3 %, the two "cancellations" read 0.63 and 0.85 and none exists
anywhere on the sweep.

This is the mistake machine/scrub.md had just finished warning about,
committed one section later, so the appendix now carries the second half of
its own rule: a band wide enough in the units the process works in. For a
pitch shifter that unit is cents, never hertz.

What survives is filed as issue #33: mean 0.907 / worst 0.633 band energy
against the scrub's 0.988 / 0.917; the strongest spectral line sitting
5-20 Hz beside the intended pitch; and yin reading up to +35 cents sharp at
110 Hz for the +7 and +19 intervals, which is ambiguous between a real error
and a detector artifact and is written up as ambiguous.

Corrects book/src/scrub.md, book/src/machine/scrub.md and
book/PLAN-radiohead-family.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The imaging hypothesis was right. fuzz.h zero-stuffed by N in one step and
filtered once at 0.45/N normalized (0.056 at 8x), which left N-1 images for a
single filter to suppress at a corner that tightened with every doubling;
residuals entering the clipper intermodulated into exactly the non-harmonic
products the alias probe measures. ondes.h had supplied the evidence without
being built for it -- same filters, comparably hard nonlinearity, but a source
with nothing zero-stuffed, and no reversal.

The chain is now one 2x stage per doubling, each filtering at 0.225 of its own
operating rate, a corner that never tightens however deep the cascade goes.
The reversal is gone: worst step-up past 2x is a ratio of 1.017, where the old
chain ran up to 3x worse per doubling. Where 4x and 8x were merely adequate
they are now two to four orders of magnitude better (5171 Hz at 8x: 2.3e-3 to
1.9e-7). The 2x column is unchanged, as it must be -- one doubling is one
stage either way, which is the best available check that nothing else moved.
Cost: 3.16 % of a core at 8x against 3.02 %.

A second, larger error surfaced doing it. Every number in the original
write-up came from a single test tone at 3733 Hz, where 2x happens to look
best. Swept across nine tones, 2x collapses above about 6 kHz and at 10499 Hz
measures worse than no oversampling at all, because the clipper's low
harmonics already exceed the base Nyquist there. The shipped default of 2x was
safe only for material below 6 kHz. The default is now 4x, and 8x earns its
keep above about 7.5 kHz where harmonics start folding inside the 4x band.

The aliasing scenario now asserts the property the cascade delivers -- the
sequence never reverses -- and that 4x beats 2x at a bright tone, which the
old chain would have failed. fuzz.ipynb SS5 is re-executed with the nine-tone
sweep and the probe's own floor plotted. The chapter, the appendix (whose
"hypothesis that died" section becomes "a hypothesis that was right", plus a
new "one tone is not a sweep"), ondes.h's cross-reference and both plans
follow.

Still open, unchanged: whether overdrive.h is owed the same change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The recipes are what make the family a rig rather than nine objects:

- recipes/tape-and-stutter.md -- fuzz into stutter into tape echo, and why
  that order (a stutter of a distorted signal is a stutter; a distortion of
  a stuttered signal amplifies every slice edge). Carries the occupancy
  numbers, the seed contract, the bitwise bypass at density 0, and the
  material contract.
- recipes/scrub-pad.md -- the two-axis controller the object needs before it
  means anything, freeze as a six-step performance, and the one real
  constraint (keep the position at least size*(rate-1) back, or the grain
  tail runs off the front of the tape).
- recipes/ondes-rig.md -- the assembled instrument. Most of its length is
  the two hands rather than the settings, because the ribbon being linear
  in semitones and the key's bottom 45 % being silent are the two facts that
  decide whether it sounds like an ondes or like a synthesizer.

Writing them caught a documentation error the chapters had missed: a first
draft set the stutter's jump to 0.15, reading it as a probability like
reverse. It is milliseconds. Recipes are the only part of the book that must
name every control with a legal value, so they check the reference pages in
a way prose does not -- noted in the drafting record.

Also records the second waveform-register source hunt in PLAN-ondes.md. It
did not close the gate, but it leaves the next attempt further along:
Leipp's Bulletin du GAM n.60 (1972) is confirmed to exist and to be held in
the Catgut archive but is digitized nowhere reachable; the ribbon-oscillator
companion paper is out of scope by its own title; and the best new lead is
the patents (the 1928 "Perfectionnements aux instruments de musique
electriques" and FR 841.128 of 1939), which are published, expired and
schematic-bearing but which Google Patents and Espacenet both refused to
serve here. The abundant hobbyist descriptions of the register waveforms are
recorded as a trap rather than a source: none is published literature and
none gives a filter shape, so implementing from them would break both the
IP policy and the promise the header makes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
@tap
tap merged commit 015e1e9 into main Aug 18, 2026
13 of 14 checks passed
tap pushed a commit to tap/TapTools-Max that referenced this pull request Aug 18, 2026
tap/TapTools#34 merged by rebase, so the branch commits got new SHAs and the
old pin (2785d24) will become unreachable when that branch is deleted. The
pin now points at 015e1e9 on main, whose tree hash is 3758c1d9 -- identical
to the branch tip the PR was reviewed at, so this is a repoint rather than a
content change.

It also picks up the one kernel commit the pin was behind by, which is
docs-only: the three Radiohead-family recipes and the register source hunt.

All nine wrapper tests re-run against the new pin and pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
tap pushed a commit to tap/TapTools-Max that referenced this pull request Aug 18, 2026
tap/TapTools#34 merged by rebase, so the branch commits got new SHAs and the
old pin (2785d24) will become unreachable when that branch is deleted. The
pin now points at 015e1e9 on main, whose tree hash is 3758c1d9 -- identical
to the branch tip the PR was reviewed at, so this is a repoint rather than a
content change.

It also picks up the one kernel commit the pin was behind by, which is
docs-only: the three Radiohead-family recipes and the register source hunt.

All nine wrapper tests re-run against the new pin and pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants