- 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>
46 lines
3.1 KiB
Markdown
46 lines
3.1 KiB
Markdown
# Per-track playback flow — on-device test checklist
|
||
|
||
The parsing/serialization and the `_end_plan`/`_goto_target` decision logic are covered by
|
||
`tests/run.mjs` and unit tests. What still needs a real device (PM_K-1 / PM_X-1) is the
|
||
**runtime**: that the gapless seam fires stop / advance / relative-goto at the right bar.
|
||
|
||
## Setup
|
||
|
||
1. Flash the new firmware: copy `dist/app.mpy` (PM_K) or `dist/explorer-app.mpy` (PM_X) onto the
|
||
device as `/app.mpy` (or use the editor's one-click push — it serves this build).
|
||
2. Author the test tracks in the web editor: paste each program string into the program-string
|
||
field, **Save** it as a set-list item, then **Save to device**. (Multi-item cases: create the
|
||
items in order in one set-list.)
|
||
3. On the device, select the set-list and press play (Button A).
|
||
|
||
`b<n>` sets the cycle length in bars; a cycle = `b<bars>`, else one master bar. The action fires
|
||
after `rep × cycle` bars (rep defaults to 1).
|
||
|
||
## Cases
|
||
|
||
| # | Track(s) | Expect | ✓ |
|
||
|---|----------|--------|---|
|
||
| 1 | `t120;kick:4` | **Loops forever.** Joystick-right advances manually. | ☐ |
|
||
| 2 | `t120;b2;kick:4;end=stop` | Plays **2 bars, then stops** by itself (LED → green, transport stops). | ☐ |
|
||
| 3 | `t120;b2;kick:4;rep=3;end=stop` | Plays **6 bars (3×2), then stops.** | ☐ |
|
||
| 4 | A=`t120;b2;kick:4;end=next` B=`t100;b2;snare:4` | A plays 2 bars then **gaplessly advances to B** (no gap, no count-in, tempo jumps to 100). | ☐ |
|
||
| 5 | A=`t120;b2;kick:4;rep=2;end=next` B=`t100;b2;snare:4` | A plays **4 bars** (2×2) then advances to B. | ☐ |
|
||
| 6 | 1=`t120;b2;kick:4` 2=`t120;b2;snare:4;end=next` 3=`t120;b2;clap:4;end=-2` | 3 jumps **back 2 → track 1** after its cycle (a looping verse→chorus section). | ☐ |
|
||
| 7 | 1=`t120;b2;kick:4;end=-2` (first item) | `end=-2` before the start **clamps to track 1** → it just loops itself. | ☐ |
|
||
| 8 | last item `…;end=next` | Advancing past the last item **wraps to the first** (set-list loops). | ☐ |
|
||
| 9 | any of the above, mid-flow | **Manual joystick-right / footswitch always advances immediately,** regardless of `end=`. | ☐ |
|
||
| 10 | global **Continue ON** + an item `t120;b2;kick:4;end=stop` | The item **still stops** — explicit `end=` overrides the global Continue default. | ☐ |
|
||
| 11 | global **Continue ON** + `t120;b2;kick:4` (no `end=`) | **Advances** after 2 bars — legacy behavior preserved (Continue = default `end=next`). | ☐ |
|
||
|
||
## What to watch for
|
||
|
||
- **Seam quality** (cases 4–6): the swap should be click-on-the-beat with no audible gap, dropout,
|
||
or double-trigger at the boundary. This is the highest-risk part of the change.
|
||
- **Stop cleanliness** (cases 2–3): no extra click after the stop; speaker goes quiet; the play
|
||
session is logged.
|
||
- **Ramp/trainer interaction**: a track with `rmp…`/`tr…` *and* an `end=` should ramp/gap normally
|
||
and still fire the end-action at `rep × cycle`.
|
||
- **Memory**: relative-goto and advance both go through `_prepare_next` (which `gc.collect()`s
|
||
before parsing). Watch for any MemoryError fallback (it leaves the track looping instead).
|
||
|
||
Report any case that misbehaves with its number and what happened.
|