Editor controls for playback flow + close web-side divergences
- 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>
This commit is contained in:
parent
42eefdf250
commit
8bba218f67
6 changed files with 133 additions and 18 deletions
46
docs/playback-flow-test.md
Normal file
46
docs/playback-flow-test.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -209,11 +209,11 @@ Surfaced by the runner. **Resolved** (now identical on web + firmware, verified
|
||||||
volume knob and no count-in, so it parses past them and does not carry them. (Contrast `@db`
|
volume knob and no count-in, so it parses past them and does not carry them. (Contrast `@db`
|
||||||
gain, which the device *does* round-trip as a per-lane field even though it doesn't apply it.)
|
gain, which the device *does* round-trip as a per-lane field even though it doesn't apply it.)
|
||||||
|
|
||||||
**Still open — web side only** (`engine.js`, flagged `expectFail: ["js"]`):
|
**Resolved on the web side** (`engine.js` now matches the firmware):
|
||||||
|
|
||||||
- **Tempo clamp** — firmware clamps `t` to `[5,300]`; `engine.js` does not yet.
|
- **Tempo clamp** — `patchToSetup` clamps `t` to `[5,300]`.
|
||||||
- **Empty patch** — firmware injects a default `beep:4`; `engine.js` returns zero lanes (the
|
- **Empty patch** — `patchToSetup` defaults to a `beep:4` lane when no lanes are given. (The
|
||||||
editor guards emptiness separately, so this may stay as-is).
|
editor still shows its "no lanes" hint by checking the *raw* input for a `:` token.)
|
||||||
|
|
||||||
The capability/version handshake (the device already replies with its firmware version over
|
The capability/version handshake (the device already replies with its firmware version over
|
||||||
SysEx) should gate features so the editor can warn when a track uses something the connected
|
SysEx) should gate features so the editor can warn when a track uses something the connected
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,18 @@
|
||||||
<label style="font-size:12px">Bars <input type="number" class="num" id="segBarsIn" min="0" max="999" value="0" title="segment length in bars (0 = manual). With Continue on, auto-advances to the next item at the bar boundary." style="width:64px"></label>
|
<label style="font-size:12px">Bars <input type="number" class="num" id="segBarsIn" min="0" max="999" value="0" title="segment length in bars (0 = manual). With Continue on, auto-advances to the next item at the bar boundary." style="width:64px"></label>
|
||||||
<span class="hint" style="margin:0">0 = manual</span>
|
<span class="hint" style="margin:0">0 = manual</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" style="gap:10px; align-items:center; margin-top:6px">
|
||||||
|
<label style="font-size:12px">At end
|
||||||
|
<select class="txt" id="endAction" title="What this track does after 'rep' cycles (a cycle = Bars above, else one bar). Loop = the metronome default; explicit choices override the global Continue toggle." style="width:108px">
|
||||||
|
<option value="loop">loop forever</option>
|
||||||
|
<option value="next">next track</option>
|
||||||
|
<option value="stop">stop</option>
|
||||||
|
<option value="goto">goto ±</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style="font-size:12px" id="gotoWrap" hidden>by <input type="number" class="num" id="endGoto" min="-99" max="99" value="-1" title="relative track offset: -2 = back two (D.S.), +1 = next" style="width:52px"></label>
|
||||||
|
<label style="font-size:12px" id="repWrap">× <input type="number" class="num" id="endRep" min="1" max="99" value="1" title="cycles before the end-action fires" style="width:52px"></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -436,7 +448,7 @@ const APP_VERSION = "0.0.1-dev";
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
STATE
|
STATE
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
const state = { bpm: 120, volume: 0.7, running: false };
|
const state = { bpm: 120, volume: 0.7, running: false, rep: null, end: null };
|
||||||
const trainer = { on: false, playBars: 2, muteBars: 2 };
|
const trainer = { on: false, playBars: 2, muteBars: 2 };
|
||||||
const ramp = { on: false, startBpm: 80, amount: 5, everyBars: 4 };
|
const ramp = { on: false, startBpm: 80, amount: 5, everyBars: 4 };
|
||||||
|
|
||||||
|
|
@ -784,8 +796,30 @@ function syncPracticeUI() {
|
||||||
$("rampOn").checked = ramp.on; $("rampStart").value = ramp.startBpm; $("rampAmt").value = ramp.amount; $("rampEvery").value = ramp.everyBars;
|
$("rampOn").checked = ramp.on; $("rampStart").value = ramp.startBpm; $("rampAmt").value = ramp.amount; $("rampEvery").value = ramp.everyBars;
|
||||||
$("countTime").value = timers.totalMs > 0 ? fmtClock(timers.totalMs) : "";
|
$("countTime").value = timers.totalMs > 0 ? fmtClock(timers.totalMs) : "";
|
||||||
$("segBarsIn").value = segBars || 0;
|
$("segBarsIn").value = segBars || 0;
|
||||||
|
syncEndActionUI();
|
||||||
refreshFeatureBoxes(); renderTimers();
|
refreshFeatureBoxes(); renderTimers();
|
||||||
}
|
}
|
||||||
|
// Per-track playback flow control: state.end (null | 'stop' | int offset) + state.rep (cycles).
|
||||||
|
function syncEndActionUI() {
|
||||||
|
const end = state.end;
|
||||||
|
const action = end == null ? "loop" : end === "stop" ? "stop" : end === 1 ? "next" : "goto";
|
||||||
|
$("endAction").value = action;
|
||||||
|
$("gotoWrap").hidden = action !== "goto";
|
||||||
|
$("repWrap").hidden = action === "loop";
|
||||||
|
if (action === "goto") $("endGoto").value = typeof end === "number" ? end : -1;
|
||||||
|
$("endRep").value = state.rep && state.rep > 1 ? state.rep : 1;
|
||||||
|
}
|
||||||
|
function readEndActionUI() {
|
||||||
|
const action = $("endAction").value;
|
||||||
|
$("gotoWrap").hidden = action !== "goto";
|
||||||
|
$("repWrap").hidden = action === "loop";
|
||||||
|
const rep = Math.max(1, parseInt($("endRep").value, 10) || 1);
|
||||||
|
if (action === "loop") { state.end = null; state.rep = null; }
|
||||||
|
else if (action === "next") { state.end = 1; state.rep = rep; }
|
||||||
|
else if (action === "stop") { state.end = "stop"; state.rep = rep; }
|
||||||
|
else { state.end = parseInt($("endGoto").value, 10) || 0; state.rep = rep; }
|
||||||
|
updateCtx();
|
||||||
|
}
|
||||||
function refreshFeatureBoxes() {
|
function refreshFeatureBoxes() {
|
||||||
$("trainerBox").classList.toggle("on", trainer.on);
|
$("trainerBox").classList.toggle("on", trainer.on);
|
||||||
$("rampBox").classList.toggle("on", ramp.on);
|
$("rampBox").classList.toggle("on", ramp.on);
|
||||||
|
|
@ -1477,7 +1511,7 @@ function commitPatchField() {
|
||||||
setPatchMsg("✓ decoded set list “" + sl.title + "” (" + sl.items.length + " item" + (sl.items.length > 1 ? "s" : "") + ") — loaded item 1", true);
|
setPatchMsg("✓ decoded set list “" + sl.title + "” (" + sl.items.length + " item" + (sl.items.length > 1 ? "s" : "") + ") — loaded item 1", true);
|
||||||
} else {
|
} else {
|
||||||
const s = patchToSetup(payload); // plain-text patch
|
const s = patchToSetup(payload); // plain-text patch
|
||||||
if (!s.lanes.length) throw new Error("no lanes — try e.g. kick:4");
|
if (!payload.includes(":")) throw new Error("no lanes — try e.g. kick:4"); // raw-input check (patchToSetup itself defaults to beep:4)
|
||||||
applyPatch(payload);
|
applyPatch(payload);
|
||||||
setPatchMsg("✓ " + s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM", true);
|
setPatchMsg("✓ " + s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM", true);
|
||||||
}
|
}
|
||||||
|
|
@ -1532,6 +1566,9 @@ $("countTime").addEventListener("input", (e) => { timers.totalMs = parseTime(e.t
|
||||||
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
|
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
|
||||||
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
|
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
|
||||||
$("segBarsIn").addEventListener("input", (e) => { segBars = Math.max(0, parseInt(e.target.value, 10) || 0); renderTimers(); syncPatchSoon(); });
|
$("segBarsIn").addEventListener("input", (e) => { segBars = Math.max(0, parseInt(e.target.value, 10) || 0); renderTimers(); syncPatchSoon(); });
|
||||||
|
$("endAction").addEventListener("change", readEndActionUI);
|
||||||
|
$("endGoto").addEventListener("input", readEndActionUI);
|
||||||
|
$("endRep").addEventListener("input", readEndActionUI);
|
||||||
$("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); });
|
$("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); });
|
||||||
$("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); });
|
$("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); });
|
||||||
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });
|
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });
|
||||||
|
|
|
||||||
41
editor.html
41
editor.html
|
|
@ -317,6 +317,18 @@
|
||||||
<label style="font-size:12px">Bars <input type="number" class="num" id="segBarsIn" min="0" max="999" value="0" title="segment length in bars (0 = manual). With Continue on, auto-advances to the next item at the bar boundary." style="width:64px"></label>
|
<label style="font-size:12px">Bars <input type="number" class="num" id="segBarsIn" min="0" max="999" value="0" title="segment length in bars (0 = manual). With Continue on, auto-advances to the next item at the bar boundary." style="width:64px"></label>
|
||||||
<span class="hint" style="margin:0">0 = manual</span>
|
<span class="hint" style="margin:0">0 = manual</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" style="gap:10px; align-items:center; margin-top:6px">
|
||||||
|
<label style="font-size:12px">At end
|
||||||
|
<select class="txt" id="endAction" title="What this track does after 'rep' cycles (a cycle = Bars above, else one bar). Loop = the metronome default; explicit choices override the global Continue toggle." style="width:108px">
|
||||||
|
<option value="loop">loop forever</option>
|
||||||
|
<option value="next">next track</option>
|
||||||
|
<option value="stop">stop</option>
|
||||||
|
<option value="goto">goto ±</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style="font-size:12px" id="gotoWrap" hidden>by <input type="number" class="num" id="endGoto" min="-99" max="99" value="-1" title="relative track offset: -2 = back two (D.S.), +1 = next" style="width:52px"></label>
|
||||||
|
<label style="font-size:12px" id="repWrap">× <input type="number" class="num" id="endRep" min="1" max="99" value="1" title="cycles before the end-action fires" style="width:52px"></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -436,7 +448,7 @@ const APP_VERSION = "0.0.1-dev";
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
STATE
|
STATE
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
const state = { bpm: 120, volume: 0.7, running: false };
|
const state = { bpm: 120, volume: 0.7, running: false, rep: null, end: null };
|
||||||
const trainer = { on: false, playBars: 2, muteBars: 2 };
|
const trainer = { on: false, playBars: 2, muteBars: 2 };
|
||||||
const ramp = { on: false, startBpm: 80, amount: 5, everyBars: 4 };
|
const ramp = { on: false, startBpm: 80, amount: 5, everyBars: 4 };
|
||||||
|
|
||||||
|
|
@ -782,8 +794,30 @@ function syncPracticeUI() {
|
||||||
$("rampOn").checked = ramp.on; $("rampStart").value = ramp.startBpm; $("rampAmt").value = ramp.amount; $("rampEvery").value = ramp.everyBars;
|
$("rampOn").checked = ramp.on; $("rampStart").value = ramp.startBpm; $("rampAmt").value = ramp.amount; $("rampEvery").value = ramp.everyBars;
|
||||||
$("countTime").value = timers.totalMs > 0 ? fmtClock(timers.totalMs) : "";
|
$("countTime").value = timers.totalMs > 0 ? fmtClock(timers.totalMs) : "";
|
||||||
$("segBarsIn").value = segBars || 0;
|
$("segBarsIn").value = segBars || 0;
|
||||||
|
syncEndActionUI();
|
||||||
refreshFeatureBoxes(); renderTimers();
|
refreshFeatureBoxes(); renderTimers();
|
||||||
}
|
}
|
||||||
|
// Per-track playback flow control: state.end (null | 'stop' | int offset) + state.rep (cycles).
|
||||||
|
function syncEndActionUI() {
|
||||||
|
const end = state.end;
|
||||||
|
const action = end == null ? "loop" : end === "stop" ? "stop" : end === 1 ? "next" : "goto";
|
||||||
|
$("endAction").value = action;
|
||||||
|
$("gotoWrap").hidden = action !== "goto";
|
||||||
|
$("repWrap").hidden = action === "loop";
|
||||||
|
if (action === "goto") $("endGoto").value = typeof end === "number" ? end : -1;
|
||||||
|
$("endRep").value = state.rep && state.rep > 1 ? state.rep : 1;
|
||||||
|
}
|
||||||
|
function readEndActionUI() {
|
||||||
|
const action = $("endAction").value;
|
||||||
|
$("gotoWrap").hidden = action !== "goto";
|
||||||
|
$("repWrap").hidden = action === "loop";
|
||||||
|
const rep = Math.max(1, parseInt($("endRep").value, 10) || 1);
|
||||||
|
if (action === "loop") { state.end = null; state.rep = null; }
|
||||||
|
else if (action === "next") { state.end = 1; state.rep = rep; }
|
||||||
|
else if (action === "stop") { state.end = "stop"; state.rep = rep; }
|
||||||
|
else { state.end = parseInt($("endGoto").value, 10) || 0; state.rep = rep; }
|
||||||
|
updateCtx();
|
||||||
|
}
|
||||||
function refreshFeatureBoxes() {
|
function refreshFeatureBoxes() {
|
||||||
$("trainerBox").classList.toggle("on", trainer.on);
|
$("trainerBox").classList.toggle("on", trainer.on);
|
||||||
$("rampBox").classList.toggle("on", ramp.on);
|
$("rampBox").classList.toggle("on", ramp.on);
|
||||||
|
|
@ -1466,7 +1500,7 @@ function commitPatchField() {
|
||||||
setPatchMsg("✓ decoded set list “" + sl.title + "” (" + sl.items.length + " item" + (sl.items.length > 1 ? "s" : "") + ") — loaded item 1", true);
|
setPatchMsg("✓ decoded set list “" + sl.title + "” (" + sl.items.length + " item" + (sl.items.length > 1 ? "s" : "") + ") — loaded item 1", true);
|
||||||
} else {
|
} else {
|
||||||
const s = patchToSetup(payload); // plain-text patch
|
const s = patchToSetup(payload); // plain-text patch
|
||||||
if (!s.lanes.length) throw new Error("no lanes — try e.g. kick:4");
|
if (!payload.includes(":")) throw new Error("no lanes — try e.g. kick:4"); // raw-input check (patchToSetup itself defaults to beep:4)
|
||||||
applyPatch(payload);
|
applyPatch(payload);
|
||||||
setPatchMsg("✓ " + s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM", true);
|
setPatchMsg("✓ " + s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM", true);
|
||||||
}
|
}
|
||||||
|
|
@ -1519,6 +1553,9 @@ $("countTime").addEventListener("input", (e) => { timers.totalMs = parseTime(e.t
|
||||||
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
|
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
|
||||||
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
|
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
|
||||||
$("segBarsIn").addEventListener("input", (e) => { segBars = Math.max(0, parseInt(e.target.value, 10) || 0); renderTimers(); });
|
$("segBarsIn").addEventListener("input", (e) => { segBars = Math.max(0, parseInt(e.target.value, 10) || 0); renderTimers(); });
|
||||||
|
$("endAction").addEventListener("change", readEndActionUI);
|
||||||
|
$("endGoto").addEventListener("input", readEndActionUI);
|
||||||
|
$("endRep").addEventListener("input", readEndActionUI);
|
||||||
$("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); });
|
$("continueMode").addEventListener("change", (e) => { continueMode = e.target.checked; lsSet(LS.continue, continueMode); });
|
||||||
$("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); });
|
$("timersOn").addEventListener("change", (e) => { timersOn = e.target.checked; lsSet(LS.timers, timersOn); refreshFeatureBoxes(); renderTimers(); });
|
||||||
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });
|
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });
|
||||||
|
|
|
||||||
|
|
@ -244,8 +244,9 @@ function patchToSetup(str) {
|
||||||
else if (tok.startsWith("tr")) { const [p, m] = tok.slice(2).split("/"); s.trainer = { on: true, playBars: +p || 1, muteBars: +m || 0 }; }
|
else if (tok.startsWith("tr")) { const [p, m] = tok.slice(2).split("/"); s.trainer = { on: true, playBars: +p || 1, muteBars: +m || 0 }; }
|
||||||
else if (tok.startsWith("rmp")) { const [a, b, c] = tok.slice(3).split("/"); s.ramp = { on: true, startBpm: +a || 80, amount: +b || 0, everyBars: +c || 1 }; }
|
else if (tok.startsWith("rmp")) { const [a, b, c] = tok.slice(3).split("/"); s.ramp = { on: true, startBpm: +a || 80, amount: +b || 0, everyBars: +c || 1 }; }
|
||||||
else if (tok.startsWith("b")) s.bars = parseInt(tok.slice(1), 10) || 0; // segment bar-length
|
else if (tok.startsWith("b")) s.bars = parseInt(tok.slice(1), 10) || 0; // segment bar-length
|
||||||
else if (tok.startsWith("t")) s.bpm = parseInt(tok.slice(1), 10) || 120;
|
else if (tok.startsWith("t")) s.bpm = Math.max(5, Math.min(300, parseInt(tok.slice(1), 10) || 120)); // clamp like the firmware
|
||||||
}
|
}
|
||||||
|
if (!s.lanes.length) s.lanes.push(laneStrToCfg("beep:4")); // a patch always has >=1 lane (match the firmware default)
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
12
tests/fixtures/track-format.json
vendored
12
tests/fixtures/track-format.json
vendored
|
|
@ -511,11 +511,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tempo-clamp-high",
|
"id": "tempo-clamp-high",
|
||||||
"status": "divergence",
|
"status": "stable",
|
||||||
"expectFail": [
|
"note": "Firmware clamps t to [5,300]; engine.js now clamps too. Spec = clamp everywhere.",
|
||||||
"js"
|
|
||||||
],
|
|
||||||
"note": "Firmware clamps t to [5,300]; engine.js does not. Spec = clamp everywhere.",
|
|
||||||
"in": "t999;kick:4=Xxxx",
|
"in": "t999;kick:4=Xxxx",
|
||||||
"norm": {
|
"norm": {
|
||||||
"bpm": 300,
|
"bpm": 300,
|
||||||
|
|
@ -542,10 +539,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "empty-defaults-to-beep",
|
"id": "empty-defaults-to-beep",
|
||||||
"status": "divergence",
|
"status": "stable",
|
||||||
"expectFail": [
|
|
||||||
"js"
|
|
||||||
],
|
|
||||||
"note": "Firmware injects a default beep:4; engine.js returns no lanes (editor guards emptiness). Spec = beep:4.",
|
"note": "Firmware injects a default beep:4; engine.js returns no lanes (editor guards emptiness). Spec = beep:4.",
|
||||||
"in": "",
|
"in": "",
|
||||||
"norm": {
|
"norm": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue