The seven components the Eno machines open up into - #66
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Seven new objects that are
tap.airport~andtap.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 havetap.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. Chaintap.gardener→tap.scale→tap.bloom→tap.chime~and you havetap.garden~with the lid off.tap.bloomis the one with reach past the family: it recirculates notes and knows nothing about chimes, so it will drivemakenote,poly~, a sampler or MIDI out just as happily.Verification
76
ctestcases 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
submodules/taptools→d4cf28a, onmain. 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.tap.chime~is the whole sixteen-voice rack, not a mono voice forpoly~. 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; andpoly~does not exist off Max, while the kernel is meant to run anywhere.tap.chime.voices~is sixteen discrete outlets rather than onemcoutlet. Not a preference — min-api'smcsupport is inlet-side only. It setsZ_MC_INLETSand provides nomultichanneloutputs, which is the method Max requires before an external may declare a variable-channelmcoutlet. 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.bloomandtap.gardenerrun on Max's scheduler, not the audio clock. Returns land within an@intervaltick (1 ms by default) rather than on the exact sample; insidetap.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.samplesshadows Min's templatedsamplesreturn type (thesamples<N>alias), and a class namedscalecollides with Min's ownscale()utility badly enough to make the class template invalid.