Skip to content

The Radiohead family: nine externals, and the kernel pin that carries them - #67

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

The Radiohead family: nine externals, and the kernel pin that carries them#67
tap merged 9 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 nine Max externals — the Radiohead family — each as a full vertical slice: wrapper, CMakeLists.txt, unit tests, reference page and help patcher. Bumps submodules/taptools to the branch carrying their kernels (tap/TapTools#34, which must merge first).

external what it is
tap.tapecho~ multi-head tape echo, one motor
tap.stammer~ live buffer-stutter rig
tap.fuzz~ two-stage fuzz with a tone stack
tap.scrub~ granular scrub pad over live capture
tap.touche~ the ondes Martenot intensity key as a gain law
tap.metallique~ / tap.palme~ the ondes diffuseurs, as standalone driven resonators
tap.triode~ one triode gain stage from a published tube model
tap.ondes~ the ondes Martenot voice

Why

REVIVAL.md had the port consolidated on main with the frontiers named; this is the first wave of new objects rather than revivals, and it follows the split the repo is built around — every line of DSP lives in the kernel repo, and each .cpp here is Min glue and nothing else. The largest wrapper is under 230 lines and most of that is attribute documentation.

Two of the nine are deliberately standalone rather than folded into tap.ondes~: the diffuseurs are resonating loudspeakers, and a resonating loudspeaker does not care what drives it — a guitar into tap.palme~ is the best argument for shipping them separately.

Verification

Built and run here (Linux):

  • All nine wrapper test binaries built and run against the mock kernel, all passing: tapecho 22 assertions / 6 cases, stammer 20/5, fuzz 22/5, touche 13/4, metallique 23/4, palme 18/4, scrub 23/5, triode 16/4, ondes 30/5.
  • pre-commit clang-format clean on every commit.
  • The kernel's own suite at the pinned tree: 322 cases, 1 758 389 assertions, all passing.

Wrapper tests earn their keep here — they are not duplicates of the kernel's. tap.ondes~'s test asserts the thing a patcher would file as a bug, that a key at rest is exactly silent, and it failed, exposing a real kernel defect: voice::set_smooth_ms never forwarded to touche::key, so a key at zero kept sounding for 20 ms. Fixed in the kernel with its own scenario. The two-layer split doing exactly what it is for.

Not done here, stated plainly:

  • No in-Max runtime validation. None of the nine has been loaded in a licensed Max, and no runtime-tests/patchers/*.maxtest.maxpat starters exist for them yet. The help patchers are generated and structurally validated (every patchline endpoint checked against declared inlet/outlet counts) but have not been opened. This is the real gap in this PR and it needs a Mac.
  • CI is the first gate for the builds. Nothing here has been compiled as an actual external — no .mxo, no .mxe64 — so the macOS universal check and the Windows build are genuinely first exercised by CI on this PR.
  • clang-tidy likewise. It runs in the style workflow with a clang front end that is not available here; it is green on the branch's pushes, which is the only evidence I have for it.

Notes for the reviewer

  • Submodule pin moved. submodules/taptools → the tip of the matching branch in tap/taptools. Merge The Radiohead family: nine kernels, twelve chapters, three recipes TapTools#34 first, then this pin needs repointing at the identical tree on main so it stays reachable after that branch is deleted.

  • Contract change: tap.fuzz~'s @oversample default moves 2 → 4. The kernel's resampler was rebuilt as a cascade of 2× stages (the single zero-stuff by N was making more oversampling measure worse), and separately the old default turned out to have been generalized from a single test tone at 3733 Hz — swept properly, 2× collapses above about 6 kHz. The attribute default now reads kernel::k_default_os rather than a literal, so the two cannot drift apart again. tap.fuzz~ has only ever existed on this branch, so no released patch is affected.

  • Max/Pd package. A reference page and a help patcher ship for all nine. The macOS universal check is unchanged and still enforced by CI.

  • Some of these link tap::dsp, some do not. tap.tune~ and the spectral trio already did; nothing new here needs it — the family's kernels are stdlib-only, including the ondes tube solve.

  • Naming. tap.metallique~ and tap.palme~ take the instrument's own French names for its diffuseurs, which are descriptive rather than anyone's trademark. The scrub deliberately avoids the obvious product association in its name, following the Bloom → garden precedent.

  • REVIVAL.md carries entries 18–23 for this wave; its progress log is current as of tap.ondes~.


Generated by Claude Code

claude and others added 8 commits August 15, 2026 23:16
The multi-head tape echo: one record head, a span of moving tape, and up
to four playback heads along it, summed to stereo. span is the motor -- the
delay of a head at the far end of the path -- so it moves the whole layout
together and bends pitch on the way, the tap.discreet~ doppler contract. The
default four evenly spaced heads are a nominal layout, not measured from any
unit; ratios builds a Copicat-style three in one message.

It carries the tape family's inversion further, which is why it exists.
tap.delay~ caps feedback at 0.99 and tap.discreet~ reaches exactly 1.0; here
regen goes past 1.0 into deliberate sound-on-sound self-oscillation, bounded
by the drive saturator rather than a gain cap -- and capped back to 1.0 per
sample whenever drive is 0, so dropping drive mid-howl lands the loop instead
of letting it run away.

The kernel's own suite carries the DSP, including the bitwise null test
against delay.h's multitap. Here: six min-api scenarios for the wrapper
surface (defaults, the past-unity regen ceiling that tap.discreet~ does not
have, the head layout reaching the tape, bitwise-dry mix, clear), the
reference page, the help patcher, and the submodule pin bumped to the kernel
commit that adds tapecho.h.

First object of the Radiohead family planned in the kernel repo's
book/PLAN-radiohead-family.md; REVIVAL.md carries the entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The second Radiohead-family object and the one with the most direct lineage
to this package: a Max stutter patch arriving as a Max object. The input is
captured continuously and, on a step grid, the machine rolls dice and
re-fires a slice of what just went past -- density how often it grabs,
divisions how finely it chops, repeats how long it holds on, reverse the
per-repeat chance of running backwards, jump how far back it may reach.

An original design in the brassage tradition, not a port of anyone's patch.

The kernel's own suite carries the DSP, including the pinned-dice identity
(one outcome possible, so the machine must reduce to exactly a one-step
delay, bitwise). Here: five min-api scenarios covering the documented
defaults, the dice clamps, and the two contracts a patcher can observe
without a scope -- at density 0 the object is a bitwise bypass whatever the
seed and whatever the mix, and the same seed replays the same performance
through the wrapper, with clear rewinding the stream.

Also the reference page, the help patcher, the REVIVAL.md entry, and the
kernel pin bumped to the commit that adds stammer.h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Two cascaded clipping stages and a bass/contrast/treble voicing section --
the harder, more scooped school beside tap.overdrive~'s feedback
soft-clipper. A recreation of a circuit class on the Yeh/Abel/Smith DAFx-07
simplified cascade, not a component model of any one pedal.

Five min-api scenarios cover the documented defaults, the control clamps,
oversample snapping, the silence-in-silence-out contract that holds even
wide open and asymmetric, and that the gain knob actually changes the
output. The oversample default of 2 is pinned deliberately: it is a measured
choice rather than a typo, since every factor beats 1 by orders of magnitude
but the sequence is not monotone and 2 comes out best.

Also the reference page, the help patcher, a REVIVAL.md entry that records
the two measurement findings (cascade gain staging, and the house
oversampling filter being too shallow here), and the kernel pin bumped to
the commit carrying fuzz.h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
The first piece of tap.ondes~ to land, shipping alone because a measured
expressive gain law is useful on anything -- as a swell pedal on a guitar,
or as the last VCA in any chain.

Its contract is unusual for the package: rather than making design choices
and measuring them, this object is obliged to reproduce someone else's
measurement. Quartier et al. (Acta Acustica 2015) published the six nuance
boundaries across the key's travel on ondes No. 320, and the kernel
interpolates those seven points to within 6e-5 dB.

The wrapper takes position from a signal on the right inlet (the performance
surface) or from an attribute, in normalized travel or in the published
millimetres. Four min-api scenarios cover the defaults, the clamps, and the
two behaviours a patcher would otherwise file as bugs: a key at rest is
exactly silent, and so is the bottom 45% of the throw -- which is the key
bending before it reaches the powder bag, not a dead spot in the object.
The help patcher and the reference page both say so plainly, because the
first reaction to that dead travel is reasonably going to be "this is
broken".

Also the REVIVAL.md entry and the kernel pin bumped to the commit carrying
touche.h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Three vertical slices over the kernel commit that added diffuseur.h and
scrub.h: wrappers, min-api tests, reference pages, help patchers, the pin
bumped to match, and REVIVAL entry 22.

tap.metallique~ and tap.palme~ are the Ondes Martenot's resonating
loudspeakers, as driven resonators rather than struck ones — there is no
trigger, the input excites the body continuously, and the moving-iron
transducer sits UPSTREAM of the body because that is the order the instrument
wires them. Both ship as standalone effects because that is where their value
is: run a guitar through the palme. Both reference pages say plainly what is
published and what is not — the mode data is Fletcher & Rossing's general
physics, not a measurement of Martenot's instruments, and the string tuning
and the two nonlinear coefficients are design choices.

tap.scrub~ is a granular scrub pad over live capture, with position and pitch
as two independent signal inlets — the two axes of a pad. Its tape is
tap.stammer~'s capture class itself. The delay null the object rests on is
re-pinned at the wrapper so a plumbing mistake cannot hide it, expressed in
samples rather than milliseconds because the mock harness does not necessarily
report 48 kHz.

14 min-api scenarios across the three. The scrub's reference page states the
transposition's warble with its measured numbers rather than leaving a patcher
to discover it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Two vertical slices over the kernel commits that added ondes.h: wrappers,
min-api tests, reference pages, help patchers, the pin bumped, and REVIVAL
entry 23.

tap.triode~ is one common-cathode valve stage solved on its load line, and it
is almost entirely a citation: the enhanced Norman Koren model with parameter
sets fitted to the valves in ondes Martenot No. 169, plus each published stage's
supply, cathode resistor and plate load. Its reference page says which side of
that line every number falls on.

tap.ondes~ is the voice — a heterodyne envelope detector into two valve stages
into the intensity key. A source, not an effect: its help patcher wires a
tap.palme~ after it, because that is how the instrument works. The two signal
inlets are the two hands, the ribbon is semitones above A1 because the published
law is linear in semitones, and nothing quantizes.

The wrapper tests earned their keep: one asked for silence at a rest key
position and got 20 ms of sound, which turned out to be the voice's anti-zipper
setting never reaching the intensity key's own slew. Fixed in the kernel and
pinned there.

Both objects document what is missing as carefully as what is present — the
waveform registers are in none of the sources obtained, and the header, the
reference page and the help patcher all say so rather than inventing them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Picks up the six book chapters for tap.scrub~, the diffuseurs and the Ondes
(docs only) and the umbrella header's include sort. No DSP change, so no
wrapper change — this keeps the pin on a commit that stays reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HKD7onDgpfjRi2PA8mhn5
Bumps the kernel pin to pick up cascaded 2x resampling in fuzz.h. The
single zero-stuff-by-N chain was what made 4x and 8x measure worse than 2x;
cascading removes the reversal, and the shipped default of 2x turned out to
have been generalized from a single test tone, collapsing above about 6 kHz.

The attribute default now tracks kernel::k_default_os rather than a literal,
so the two cannot drift again. Wrapper test, object description and maxref
follow.

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

tap commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

The macOS job went red, and it is not this PR. Recording it here rather than leaving it to be inferred from a re-run.

What failed: 19 - tap.bloom_test (Bus error), 1 of 85, on 6115aea. Every other check on this PR is green, including clang-tidy, clang-format, drift and the Windows build.

Why it is not mine:

  • tap.bloom is not in this diff, and neither is garden.h, its only kernel dependency. This PR's diff touches tap.fuzz~'s wrapper, its maxref, its test, and the submodule pin.
  • The same SHA passed this same job three hours earlier on its push run (23:04 UTC), and failed on the pull_request run (01:47 UTC). Same tree, same job, different outcome.
  • tap.bloom_test ran 40 consecutive times on Linux without failing.

It is a real bug, though, so it is filed rather than shrugged off: #68. There is a second data point — tap.gardener_test failed the same job on 08a9671 two days ago — and tap.bloom and tap.gardener are the package's only two objects that arm a self-rescheduling timer<> from their constructor. The likely mechanism is the harness servicing a timer belonging to a test_wrapper that has already been destroyed, which would explain a memory fault in a test that only sets attributes. That is a hypothesis; the issue says so and says what would confirm it.

I have re-run the failed job. If it goes green, this PR is unaffected and #68 carries the flake. If tap.bloom_test fails again on a re-run, that changes the picture and I will say so here rather than re-running a third time.


Generated by Claude Code

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 commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

tap/TapTools#34 has merged, so the merge-order step this PR's description called for is done: the pin now points at 015e1e9 on main rather than a branch commit that would go unreachable when the branch is deleted.

It merged by rebase, so the branch commits got new SHAs. 015e1e9's tree hash is 3758c1d9 — byte-identical to the branch tip this PR was reviewed against — so it is a repoint, not a content change. It also picks up the one kernel commit the pin had been behind by, which is docs-only (the three recipes and the register source hunt).

All nine wrapper tests re-run against the new pin and pass. This PR is now ready on its own terms.


Generated by Claude Code

@tap
tap merged commit 74c1044 into main Aug 18, 2026
10 checks passed
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