- docs/playback-flow-test.md: on-device verification checklist for the runtime
(stop / rep / next / relative-goto / boundary / manual-override cases).
- editor.html + editor-beta.html: graphical "At end" control (loop / next / stop /
goto ±N) plus a rep-count input in the arrangement panel, wired through
state.rep/state.end -> currentSetup/currentPatch. Authoring is no longer
text-field-only.
- src/engine.js: patchToSetup now clamps tempo to [5,300] and defaults to a beep:4
lane when no lanes are given, matching the firmware. The editors keep their
"no lanes" hint by checking the raw input for a ':' token instead of parsed lanes.
- fixtures: tempo-clamp-high + empty-defaults-to-beep now pass on both engines.
Suite: 41 pass / 1 known (only the intentional vol/cd host boundary remains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the per-track end-action model designed in docs/track-format.md §3, end to
end across both engines, both firmwares, and the editors.
Grammar (parsed + serialized by engine.js and both app.py):
rep=<n> cycles before the end-action fires (default 1)
end=stop stop after rep cycles
end=next advance one track (sugar for end=+1)
end=<±N> relative goto after rep cycles (e.g. end=-2 = D.S.)
(absent) loop forever — the metronome default
Firmware runtime (pico-cp + pico-explorer): _on_new_bar now consults a per-track
_end_plan() and fires stop / gapless-advance / relative-goto at the right bar.
A cycle = b<bars>, else one master bar; fire bar = rep * cycle. Explicit end=
governs; with no end, the global Continue toggle stays a default (=end=next, still
needs b<bars>) so existing set-lists and the CONT UI are unchanged. _prepare_next
takes a target index; the seam machinery, _do_advance and live-sync all carry rep/end.
Editors (editor.html + editor-beta.html): state.rep/state.end thread through
applySetup / currentSetup / currentPatch so load -> edit -> save preserves the
flow; authoring is via the program-string field (no graphical control yet).
Tests: the 3 playback-flow vectors now pass on both engines (39 pass / 3 known).
Runtime decision logic (_end_plan / _goto_target) unit-tested for stop, rep,
relative goto clamp/wrap, and legacy-Continue precedence. Codec round-trip
verified idempotent. Both firmwares compile + mpy-cross clean.
Also: untrack stale __pycache__/*.pyc build artifacts and gitignore them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close three real parser divergences the conformance suite flagged on the device
side (pico-cp + pico-explorer) — cases where the firmware produced a different
groove/sound than the web for the same patch:
- Euclidean (k,n,rot) shorthand (e.g. kick:4(3,8)) — was silently dropped to a
plain bar; now expands to the same hits as engine.js (added _euclid + parsing).
- GM note-number lane sounds (e.g. 36:4) — now resolve to the voice name (GM_NUM).
- Unknown sound names fall back to beep, matching the web.
vol/cd are NOT carried by the firmware by design: they are web-authoring fields
(the device has a hardware volume knob and no count-in). Documented as an
intentional, permanent host difference rather than a bug; the vol-and-countin
vector stays as expectFail[py] to mark the boundary.
tests/adapters/py_adapter.py: extract the new SOUND_GM/GM_NUM/_euclid nodes.
fixtures: euclid/unknown-sound/gm-note-number now pass on both engines.
docs §6 updated. node tests/run.mjs: 33 pass / 9 known, round-trips stable.
pico-explorer parser spot-checked identical to pico-cp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A lane with no =pattern produced different defaults on web vs device — a real,
shipped divergence the new conformance suite caught (e.g. hatClosed:4/2 in
"Four-on-the-floor" played steady 8ths in the browser but quarter-notes on the
device). Adopt one rule everywhere: every subdivision sounds at normal level,
accents fall ONLY on group starts (the grouping is the accent map).
- pico-cp/app.py, pico-explorer/app.py: off-beat subdivisions sound at normal (1)
instead of resting (0); group-start accenting was already correct.
- src/engine.js: default beatsOn accents group starts only (was: every beat);
laneCfgToStr isDefault check updated to match so round-trips stay idempotent.
- docs + fixtures: document the rule; default-pattern vectors now pass on both.
Audible effect (intended): device subdivided hat/ride lanes gain their off-beat
strokes (now match the web); web stops over-accenting every beat. Lanes with an
explicit =pattern are unchanged. Verified green via node tests/run.mjs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single source of truth for the track ("program"/"patch") grammar, which was
implemented by hand in src/engine.js and pico-cp/app.py with no cross-check and
had quietly drifted.
- docs/track-format.md: formal grammar, container (programs.json) schema with a
version field, the new per-track playback-flow model (rep/end + relative goto;
default = loop forever), normalization rules, and a list of known divergences.
- tests/: golden vectors + a runner that loads the REAL engine.js and app.py
grammar (no copies; app.py via ast extraction) and compares both against the
spec. Exit non-zero on unexpected mismatch or round-trip break -> usable as CI.
Surfaces real divergences for follow-up: default accent pattern (no =pattern)
differs web vs device and affects shipped presets; euclid not parsed on device;
vol/cd dropped on device; unknown-sound fallback; tempo clamp; empty patch.
The rep/end playback-flow vectors are the acceptance test for building that.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>