sequencer.py: one AMY tag per Sequence (pairs with amy#1156) - #1352
Conversation
Bumps the amy pin to a83c27bd, where events sent to a sequencer tag ACCUMULATE instead of replacing what was there. Two things follow. The bug first: AMYSequenceEvent.update() keeps its tag across calls, so re-editing an already-scheduled step re-sent ticks= to a tag that still held the old version. Under accumulate that stacks, and both versions play. The rest is the redesign that accumulate makes possible. An AMYSequence now takes ONE tag at construction and schedules every step under it, where each step used to burn a tag of its own -- which ran AMY's 256-tag space out after a few minutes of editing (300 step edits now consume 0 tags). The cost is that AMY can only erase a tag whole, so editing or removing a step rebuilds the tag from the surviving events; add() still costs one message, since adding is exactly what accumulate does, and clear() is now a single message whatever the sequence holds. A naive edit loop is then N**2 messages, so AMYSequence.batch() coalesces a run of edits into one rebuild. drums.py's DrumRow.update_switches() is the one loop that hits it: a row voice change over a filled 16-step row drops from 152 messages to 17. tulip/shared/test_amy_sequencer.py drives the real AMYSequence against a real AMY (stubbing the two firmware-only imports, reading back what AMY holds via its own debug dump) and pins all of the above, including the batched message counts. refdocs/amy refreshed for the new pin, per the AMY submodule policy. NOTE: sequencer.py now depends on the accumulate semantics and will misbehave quietly against an older amy -- each add() would replace rather than accumulate, so a pattern would play only its last step. It has to move with the pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MIDI-out example was the only place in the AMYboard docs that shows a tagged ticks=, and it predates events accumulating on a tag. It put its note-on and note-off on two tags, which reads as "a tag holds one event" -- the rule that just changed. Puts both on tag 7 instead, which is what a tag is for now, and explains what follows: sends to a tag accumulate, ticks="0,0,<tag>" drops the whole tag, there is no way to remove one event from a tag, the clear needs its own send because ticks= claims the rest of its message, and same-tick events play in tag order then add order. Links out to amy's synth.md for the rest. Also points at sequencer.AMYSequence for callers who would rather not manage tags by hand, since it now takes one tag and does the clear-and-rebuild itself. Every snippet was run against a real AMY. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔌 AMYboard PR previewEditor + flasher: https://amyboard-pr-1352.vercel.app/editor/ This preview bundles this PR's firmware — its flasher only flashes this build (not the release). Rebuilt on every push; removed when the PR closes. The hardware CI has been kicked off and should return within a few minutes, stand by! |
🌷 Tulip Web PR previewTulip Web: https://tulip-pr-1352.vercel.app/run/ Flash a Tulip to this build: on-device run Rebuilt on every push; removed when the PR closes. |
amy#1156 merged as 0c05eba, and the release workflow then bumped the version to 1.2.166. Moves the pin from the PR-branch commit (a83c27bd) to the current tip of amy main, per the submodule policy. a83c27bd was not dangling -- #1156 landed as a merge commit, not a squash, so the old pin was still an ancestor of main -- but it was not the latest known working version, which is what the policy asks for before tulipcc goes to main. The two bump commits changed only the version string in amy/__init__.py, docs/amy.js, docs/amy.wasm, library.properties and pyproject.toml. No docs/*.md changed, so the refdocs snapshot is byte-identical and only its recorded source SHA moves; _KW_MAP_LIST is untouched, so amy_kwmap.h needs no regeneration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🎛️ HW CI (physical bench)AMYboard (USB-MIDI + AMY Tulip (TULIP4_R11; serial-REPL audio + WiFi screenshot): ✅ PASS — flashed this PR’s firmware; all checks matched the references. ⬇️ Artifacts: recordings · screenshot · serial logs · run logs Self-hosted bench. Audio spectral-compared to |
Pairs with shorepine/amy#1156, where events sent to an AMY sequencer tag now accumulate instead of replacing what was there.
The bug
AMYSequenceEvent.update()keeps its tag across calls, so re-editing an already-scheduled step re-sentticks=to a tag that still held the previous version. Under accumulate that stacks and both versions play. Before the fix, editing one step of a three-step sequence gives four entries:The redesign accumulate makes possible
An
AMYSequencenow takes one tag at construction and schedules every step under it. Each step used to burn a tag of its own, which ran AMY's 256-tag space out during ordinary use — 300 step edits now consume 0 tags, where before they consumed 300.The cost is that AMY can only erase a tag whole, never one entry in it, so editing or removing a step rebuilds the tag from the surviving events.
add()still costs one message, since adding is exactly what accumulate does, andclear()is now a single message whatever the sequence holds.That makes a naive edit loop
N**2messages, soAMYSequence.batch()coalesces a run of edits into one rebuild.drums.py'sDrumRow.update_switches()is the one loop that hits it — a row voice change over a filled 16-step row drops from 152 messages to 17.Contents
tulip/shared/py/sequencer.py— the above.tulip/shared/py/drums.py— batch the row-voice-change loop.tulip/shared/test_amy_sequencer.py— new host-side test (below).docs/amyboard/python.md— the MIDI-out example was the only taggedticks=in the AMYboard docs and put its note-on/note-off on two tags, which now reads as teaching the rule that changed. Both on one tag, plus what follows from accumulate.tulip/server/refdocs/amy/— refreshed for the new pin, per the AMY submodule policy.Testing
tulip/shared/test_amy_sequencer.pyfollowstest_amyboard_encoder.py's pattern: stub the two firmware-only imports, drive the realAMYSequenceagainst the pip-installedamy, and read back what AMY actually holds via its owndebug=6dump. 24 checks — the shared tag, two sequences not colliding, batched vs unbatched message counts, a mixed edit/add/remove batch matchingdrums.py's real usage, single-messageclear(). Reverting the fix fails 3 of them.I also replayed AMYboard World sketch #1139 (
seinfeld_theme.py, the densestAMYSequenceuser on World: 3 sequences, 31 adds) against the new code:It also has four drum events colliding on one step. AMY fires them in add order, which is what ascending-tag order gave before — playback is unchanged.
Sketch audit
I scanned all 1486 AMYboard World sketches (586 live) plus the in-repo ones for anything exposed to the accumulate change:
ticks="a,b,c"H…wire stringsequence=kwargticks=<expr>sequencer.AMYSequencesequencer.pytulip.seq_*Nothing on World breaks. All 10 live
AMYSequenceusers call only.add(), which is the cheap path.Note on the coupling
sequencer.pyfails quietly against an older amy: eachadd()would replace rather than accumulate, so a pattern would play only its last step. These changes have to move with the pin.🤖 Generated with Claude Code