Repository navigation
v2.8.0 — Live macros stop stranding state; new control-surface e2e suite
Fixes the "live macros and beat pads stopped working" class of bugs, and adds a headless-Chrome suite that exercises every control in the studio so this can't regress silently again.
Root cause — not the Tone 15 upgrade
The suspicion was v2.7.0's Tone.js major bump. It wasn't: the engine, render, MIDI, and voice-recycle paths were audited against the pinned tone@15.1.22 bundle and are clean.
The real cause was cancel paths that cancelled in-flight macro animations without restoring their pre-fire state. Stop, Panic, and project-sync each carried their own divergent cleanup — Panic restored, the other two didn't. Interrupting a macro therefore left the studio in whatever half-swept state the animation was in:
- Stop mid Sweep HP → master highpass stranded up, mix goes thin and near-silent.
- Stop mid Tape Stop → tempo stuck at the crawl.
- Stop mid Build Up / Wind Down → Feel puck stranded mid-sweep — and the puck persists to
localStorage, so this one survived reload and left the studio permanently mis-tuned. This is why it read as "the app is broken" rather than "that macro glitched."
A second, independent bug compounded it: ticking any one-shot Repeat box routed a fire through the serial macro queue every beat (~500 ms at 120 BPM) against a ~700 ms queue budget, so the queue grew without bound and Auto-DJ's busy check reported "skipped — busy" for the rest of the session.
Fixes
- Every animation token (FX sweep/hold, tempo, feel, channel pan/decay) carries its own restore closure.
clearLocalMacroStateis now the single canceller, and Panic / Stop / project-sync all delegate to it — one behavior, three call sites. - Stop halts
Tone.Transportbefore cancelling macros, sorestoreFeelState's reverb flush takes its synchronous branch instead of scheduling a restore that the stop then deletes (reverb-wet stranded at 0). - Tempo macros inherit a live prior animation's
startBpm, the same guardfxSweepalready had — a double-fired Half Time no longer makes 60 BPM permanent. - Repeating one-shots, paired one-shot FX, and stacked macros bypass the serial queue instead of claiming its slot;
markMacroRunning's release timer gets a generation guard against double-dequeue. - Seamless Auto-DJ regen re-asserts the incoming project's tempo to the worker (a tempo macro straddling the boundary used to leave the new track playing at the old BPM).
- Stale Fire-pad re-mute timers are cleared on cancel, so they can't re-mute a hit track the user deliberately unmuted on a newly loaded project.
- Reverb flush publishes its in-flight target so a snapshot taken inside the 180 ms flush window (Auto-DJ stack of two Feel macros) captures the real wet value, not the transient 0.
New — make test-e2e
scripts/test-e2e-controls.mjs drives headless Chrome over raw CDP. No dependencies beyond node ≥ 20 and google-chrome — no Playwright, no Python. 150 checks, each asserting an observable effect rather than "didn't throw":
- Transport, all seven panels, mixer, Feel, Auto-DJ firing, generate / shuffle / genre, share-payload CID sealing.
- Every macro in the catalog, with per-kind effect assertions and restore assertions (tempo returns, mutes return, queue releases).
- The four Fire pads and the engine one-shots (airhorn / laser / subdrop / booj) verified audible via an analyser tap on
Tone.Destination, not just "fired." - FX sliders traced down to the actual engine
AudioParamvalues. - Keyboard shortcuts driven as real key events.
- MIDI input via a Web MIDI API mock — note-on through a fake device produces audio, so the MIDI path is covered without hardware.
- Stop-mid-macro and Panic-mid-macro restore regressions — the specific bugs above now have permanent guards.
Any uncaught exception or console.error during the run fails the suite.
Verification
- ✅ 150/150 e2e locally and against production after deploy.
- ✅
scripts/test-macro-audio.py5/5 (spectral-centroid and RMS analysis of real captured audio). - ✅
go test ./...green.
🤖 Generated with Claude Code