Skip to content

The seven components the Eno machines open up into - #66

Merged
tap merged 7 commits into
mainfrom
claude/airports-garden-components-ga28kb
Aug 15, 2026
Merged

The seven components the Eno machines open up into#66
tap merged 7 commits into
mainfrom
claude/airports-garden-components-ga28kb

Conversation

@tap

@tap tap commented Aug 15, 2026

Copy link
Copy Markdown
Owner

What this changes

Seven new objects that are tap.airport~ and tap.garden~ taken apart — tap.reel~, tap.chime~, tap.chime.voices~, tap.bloom, tap.scale, tap.gardener, tap.period.

The monoliths stay exactly as they are. This is additive: the same kernel classes, reached two ways.

Why

Sum several tap.reel~ at awkward lengths and you have tap.airport~ with every loop reachable — an insert on one phrase, a varispeed on one reel, more than eight loops, and tape bought per reel rather than eight worst-case reels whether you use them or not. Chain tap.gardenertap.scaletap.bloomtap.chime~ and you have tap.garden~ with the lid off.

tap.bloom is the one with reach past the family: it recirculates notes and knows nothing about chimes, so it will drive makenote, poly~, a sampler or MIDI out just as happily.

Verification

76 ctest cases green, clang-format and clang-tidy clean across all twelve new translation units. Wrapper tests cover defaults and the clamping each attribute owes its kernel. Rebuilt clean against the merged kernel pin.

DSP correctness is gated in the kernel repo, as usual — the identity claims these objects rest on are pinned there bitwise (airport_test.cpp "standalone lanes summed are the bank, bitwise"; garden_test.cpp "the bed is exactly its components wired together, bitwise", and "the per-voice taps summed through their seats are the stereo rack").

Not done, and worth stating plainly: none of this has been opened in Max. Everything was built and tested on Linux against the mock kernel. The help patchers and the runtime-test patcher were generated headless — structurally validated (no dangling patch cords, no out-of-range inlet or outlet indices, keys matching an existing patcher) but never loaded. Seven help patchers and one maxtest patcher need an on-Mac pass, and the universal-binary check is CI's first real gate.

Notes for the reviewer

  • Submodule pin moved. submodules/taptoolsd4cf28a, on main. The Eno family, and the components it is made of TapTools#32 merged by rebase, so the tree this was originally pinned at now lives on main under a new SHA; the tree hashes were compared and are identical, and the pin has been repointed so it stays reachable once the topic branch is deleted.
  • Max/Pd package. Reference page and help patcher ship for every new object. Universal-binary status is unverified here — CI is the first machine that can check it.
  • tap.chime~ is the whole sixteen-voice rack, not a mono voice for poly~. Two reasons, and it looks like over-engineering until you have both: poly~ steals round-robin, which loses the quietest-first steal-by-re-aim that makes a steal glide instead of click; and poly~ does not exist off Max, while the kernel is meant to run anywhere.
  • tap.chime.voices~ is sixteen discrete outlets rather than one mc outlet. Not a preference — min-api's mc support is inlet-side only. It sets Z_MC_INLETS and provides no multichanneloutputs, which is the method Max requires before an external may declare a variable-channel mc outlet. If a later Min gains it, that object is where to adopt it. It is a separate object rather than a mode because outlet count is fixed when a Min object is constructed.
  • tap.bloom and tap.gardener run on Max's scheduler, not the audio clock. Returns land within an @interval tick (1 ms by default) rather than on the exact sample; inside tap.garden~ the same ring is sample-accurate. Inaudible at loop lengths measured in seconds, but real — and it is why there is deliberately no in-Max null test for the garden. Asserting a null that cannot hold would be worse than not asserting one. The airport's null test does exist (runtime-tests/patchers/tap.reel~-is-airport.maxtest.maxpat) because both sides of that one are on the audio clock.
  • Two collisions the compiler caught, recorded so the next object avoids them: a message named samples shadows Min's templated samples return type (the samples<N> alias), and a class named scale collides with Min's own scale() utility badly enough to make the class template invalid.

claude added 7 commits August 15, 2026 00:57
tap.reel~ is one free-running tape loop; sum several at awkward lengths and you
have tap.airport~ with every loop reachable — an insert on just one phrase, a
varispeed on one reel, more than eight loops, and tape bought per reel rather
than eight worst-case reels whether you use them or not.

tap.chime~, tap.bloom, tap.scale and tap.gardener are tap.garden~ opened up:
tap.gardener into tap.scale into tap.bloom into tap.chime~ is the object,
patched. tap.bloom is the one with reach beyond the family — it recirculates
notes and knows nothing about chimes, so it will drive makenote, poly~, a
sampler, or MIDI out just as happily.

Two design points worth recording. tap.chime~ is the whole sixteen-voice rack
rather than a mono voice you wrap in poly~, because poly~ steals round-robin
and does not exist off Max; keeping the quietest-first, steal-by-re-aim
allocator in the kernel is what preserves the glide-not-click promise and every
non-Max target. And tap.bloom/tap.gardener run on Max's scheduler rather than
the audio clock, so returns land within an @interval tick instead of on the
exact sample — inaudible at loop lengths measured in seconds, but a real
difference from the monolith, stated in both headers and the reason the null
test lives in the kernel rather than in a patch.

Two collisions the compiler caught, noted so the next object avoids them: a
message named `samples` shadows Min's samples<N> return type, and a class named
`scale` collides with Min's own scale() utility badly enough to make the class
template invalid.

Wrapper tests cover defaults and the clamping each attribute owes its kernel;
all 74 ctest cases pass, and clang-format and clang-tidy are clean on the ten
new translation units.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
A reference page and a help patcher each, plus the REVIVAL progress entry.
The help patchers carry the point of the exercise: tap.reel~'s is three reels
at incommensurate lengths summed into one bus — tap.airport~ with the lid off —
and tap.gardener's is the whole chain, wind into scale into ring into rack,
which is tap.garden~ drawn as patch cords instead of prose.

Also adds a list handler to tap.chime~. It was reachable only by `note`, which
meant the bare list arriving from `route note` downstream of tap.bloom fell on
the floor — the patched garden's own wiring would not have worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
The split claims that three tap.reel~ summed ARE tap.airport~. The kernel
suite pins that bitwise, but it cannot see the wrappers — a mis-forwarded
attribute, or a dspsetup that re-prepares one side and not the other, passes
in the kernel and would still be wrong in Max.

So: a third max-test topology, null_test(), and a patcher that drives both
sides from one sig~, sums the lanes, subtracts the monolith, and requires
zero. The record gates open while the DSP chain is still off, which is what
keeps the two sides sample-aligned — no audio has been processed, so every
head is still at zero when the gates open.

No garden equivalent, on purpose. tap.bloom and tap.gardener run on Max's
scheduler rather than the audio clock, so the patched garden is the same
machine but not the same sample stream; a null test there would assert
something untrue. That identity stays pinned in the kernel, where both sides
share one clock. Both the generator and the README say so, so the absence
reads as a decision rather than an oversight.

Untested in Max: this generator runs headless, so the patcher needs opening
once on a Mac like every other one here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
Book-only change in the kernel, so the wrappers are unaffected — but the pin
should sit at the tip of the branch these objects were developed against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
Two follow-ups that close the gaps the split left.

tap.chime.voices~ is the rack with every bell on its own outlet: sixteen mono
signals, each carrying its tube dry, before the seat in the stereo image. So a
patch can filter or gate one voice without touching the other fifteen — which
is a different instrument from filtering the whole rack, and the thing
tap.chime~ cannot give you. It reports which tube each slot is holding, since
the pool reassigns bells as it steals and a slot is not a pitch.

Two constraints shaped it. Outlet count is fixed when a Min object is
constructed, so this cannot be a mode on tap.chime~ — hence a separate object.
And it is sixteen discrete outlets rather than one mc outlet because min-api's
mc support is inlet-side only: it sets Z_MC_INLETS and provides no
multichanneloutputs, which is the method Max requires before an external may
declare a variable-channel mc outlet. Both reasons are in the file header so
the next reader does not re-derive them, along with a note that this is where
to adopt mc if a later Min gains it.

tap.period closes the other gap — the composite period needs every length at
once, so it had nowhere to live in a patch of independent reels. It wraps the
same free function the bank now calls, and quantizes lengths through the same
shared kernel helper the reels use, so the two cannot disagree. That is
load-bearing rather than tidy: the lcm is over sample counts, and lengths that
look commensurate as decimals are not as samples.

Vertical slice each, 76 ctest cases green, clang-tidy and clang-format clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
Book-only change in the kernel, so the wrappers are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
tap/TapTools#32 merged by rebase, so the tree this was pinned at (fac61d3) now
lives on main as d4cf28a — identical tree, verified by comparing the tree
hashes, so nothing here is affected. Repointing before the topic branch is
deleted keeps the pin reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018s67n9Z2ENnhQaFFWJKaVe
@tap tap changed the title The Eno machines, and the seven components they open up into The seven components the Eno machines open up into Aug 15, 2026
@tap
tap merged commit 4cf0a29 into main Aug 15, 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