From da7c94e67fd916a38f7b013913ac83723b485b7b Mon Sep 17 00:00:00 2001 From: Me Here Date: Sun, 31 May 2026 00:37:06 -0500 Subject: [PATCH] Implement per-track playback flow (rep / end / relative goto) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= 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, 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) 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) --- .gitignore | 4 + docs/track-format.md | 10 +- editor-beta.html | 5 +- editor.html | 5 +- pico-cp/__pycache__/app.cpython-312.pyc | Bin 116845 -> 0 bytes pico-cp/__pycache__/code.cpython-312.pyc | Bin 894 -> 0 bytes pico-cp/app.py | 77 ++- pico-explorer/app.py | 72 ++- pico/__pycache__/main.cpython-312.pyc | Bin 72023 -> 0 bytes src/engine.js | 10 +- tests/adapters/js_adapter.mjs | 4 +- tests/adapters/py_adapter.py | 14 +- tests/fixtures/track-format.json | 600 +++++++++++++++++++--- wokwi/__pycache__/ssd1306.cpython-312.pyc | Bin 5509 -> 0 bytes 14 files changed, 686 insertions(+), 115 deletions(-) delete mode 100644 pico-cp/__pycache__/app.cpython-312.pyc delete mode 100644 pico-cp/__pycache__/code.cpython-312.pyc delete mode 100644 pico/__pycache__/main.cpython-312.pyc delete mode 100644 wokwi/__pycache__/ssd1306.cpython-312.pyc diff --git a/.gitignore b/.gitignore index af067a3..0ca36a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ # Build output — assembled from index.html + assets/ by build.sh dist/ tools/ + +# Python build artifacts +__pycache__/ +*.pyc diff --git a/docs/track-format.md b/docs/track-format.md index 6ff3dc0..0ce4302 100644 --- a/docs/track-format.md +++ b/docs/track-format.md @@ -72,7 +72,7 @@ bars = "b" int ; (* cycle length in bars; drives C trainer = "tr" int "/" int ; (* playBars "/" muteBars (gap trainer) *) ramp = "rmp" int "/" signed "/" int ; (* startBpm "/" amount "/" everyBars *) -rep = "rep" int ; (* NEW: cycles before end fires; default 1 *) +rep = "rep=" int ; (* cycles before end fires; default 1 *) end = "end" "=" ( "stop" | "next" | signed ) ; (* NEW: see §3 *) lane = sound ":" groups [ "/" sub [ "s" ] ] [ euclid ] [ "=" pattern ] @@ -115,9 +115,13 @@ pattern = *( "X" | "x" | "g" | "." | "-" | "_" ) ; (* per-step dynamics *) --- -## 3. Playback flow (NEW) +## 3. Playback flow -Replaces the old **global** `Continue` toggle with **per-track** behavior. +Per-track playback behavior (parsed + serialized by both engines; firmware runtime implemented). +**Implementation note:** the device keeps the global `Continue` toggle as a *default* — a track +with an explicit `end=` governs itself; a track without one falls back to `end=next` while +Continue is on (and still needs `b`), else it loops. So per-track `end=` overrides the +global toggle rather than replacing the UI. **Default (no `end=` token) — loop forever**, exactly like a metronome. Manual advance (joystick / footswitch) always moves to the next track. "Vamp until cue" is therefore the diff --git a/editor-beta.html b/editor-beta.html index b9b0c3e..19b77bc 100644 --- a/editor-beta.html +++ b/editor-beta.html @@ -769,13 +769,14 @@ let historyName = null; // item whose past-session history is shown let continueMode = lsGet(LS.continue, false); // auto-advance to next item when countdown ends let timersOn = lsGet(LS.timers, true); // master switch for the elapsed/countdown timers -function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs, bars: segBars }; } +function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs, bars: segBars, rep: state.rep, end: state.end }; } function applySetup(s) { setBpm(s.bpm); applyLanes(s.lanes); if (s.trainer) Object.assign(trainer, s.trainer); if (s.ramp) Object.assign(ramp, s.ramp); timers.totalMs = s.countMs || 0; timers.remainingMs = timers.totalMs; // per-item time countdown segBars = s.bars || 0; segBarCount = 0; // per-item bar-length + counter + state.rep = s.rep != null ? s.rep : null; state.end = s.end != null ? s.end : null; // per-track playback flow (preserved on round-trip) syncPracticeUI(); updateCtx(); } function syncPracticeUI() { @@ -1053,7 +1054,7 @@ function importAll(file) { Patch: v1;t;vol;;…[;tr/][;rmp//] Lane: :[/][=][~ poly][! disabled] ========================================================================= */ -function currentPatch() { return setupToPatch({ bpm: state.bpm, volume: state.volume, lanes: snapshotLanes(), trainer, ramp }); } +function currentPatch() { return setupToPatch({ bpm: state.bpm, volume: state.volume, lanes: snapshotLanes(), trainer, ramp, rep: state.rep, end: state.end }); } function setVolume(pct) { state.volume = Math.max(0, Math.min(1, pct / 100)); $("vol").value = Math.round(state.volume * 100); volVal.textContent = Math.round(state.volume * 100) + "%"; diff --git a/editor.html b/editor.html index 1915633..d00f438 100644 --- a/editor.html +++ b/editor.html @@ -767,13 +767,14 @@ let historyName = null; // item whose past-session history is shown let continueMode = lsGet(LS.continue, false); // auto-advance to next item when countdown ends let timersOn = lsGet(LS.timers, true); // master switch for the elapsed/countdown timers -function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs, bars: segBars }; } +function currentSetup() { return { bpm: state.bpm, lanes: snapshotLanes(), trainer: { ...trainer }, ramp: { ...ramp }, countMs: timers.totalMs, bars: segBars, rep: state.rep, end: state.end }; } function applySetup(s) { setBpm(s.bpm); applyLanes(s.lanes); if (s.trainer) Object.assign(trainer, s.trainer); if (s.ramp) Object.assign(ramp, s.ramp); timers.totalMs = s.countMs || 0; timers.remainingMs = timers.totalMs; // per-item time countdown segBars = s.bars || 0; segBarCount = 0; // per-item bar-length + counter + state.rep = s.rep != null ? s.rep : null; state.end = s.end != null ? s.end : null; // per-track playback flow (preserved on round-trip) syncPracticeUI(); updateCtx(); } function syncPracticeUI() { @@ -1049,7 +1050,7 @@ function importAll(file) { Patch: v1;t;vol;;…[;tr/][;rmp//] Lane: :[/][=][~ poly][! disabled] ========================================================================= */ -function currentPatch() { return setupToPatch({ bpm: state.bpm, volume: state.volume, lanes: snapshotLanes(), trainer, ramp }); } +function currentPatch() { return setupToPatch({ bpm: state.bpm, volume: state.volume, lanes: snapshotLanes(), trainer, ramp, rep: state.rep, end: state.end }); } function setVolume(pct) { state.volume = Math.max(0, Math.min(1, pct / 100)); $("vol").value = Math.round(state.volume * 100); volVal.textContent = Math.round(state.volume * 100) + "%"; diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc deleted file mode 100644 index e64e6438ae461628f2d33a99bff9f4368e37b7c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116845 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
R^PYG+27nsg3ruBlz1IIgd+v(VAX*W!^9i# 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 no unsaved edits @@ -608,6 +621,9 @@ class App: if self.ramp: parts.append('rmp%d/%d/%d' % (self.ramp.get('start', self.bpm), self.ramp['amt'], self.ramp['every'])) if self.trainer: parts.append('tr%d/%d' % (self.trainer['play'], self.trainer['mute'])) for L in self.lanes: parts.append(lane_to_str(L)) + if self.end is not None: # per-track playback flow (default = loop forever -> omitted) + if self.rep and self.rep > 1: parts.append('rep=' + str(self.rep)) + parts.append('end=' + ('stop' if self.end == 'stop' else 'next' if self.end == 1 else ('+%d' % self.end if self.end > 0 else str(self.end)))) return ';'.join(parts) # ---------- on-device editing: tap a beat to cycle it; tap the title to save/revert ---------- @@ -1055,8 +1071,8 @@ class App: try: cur = self._prog_str() except Exception: cur = None if patch and patch != cur: - bpm, lanes, bars, ramp, trainer = parse_program(patch) - self.bpm = bpm; self.lanes = lanes; self.bars = bars; self.ramp = ramp; self.trainer = trainer + bpm, lanes, bars, ramp, trainer, rep, end = parse_program(patch) + self.bpm = bpm; self.lanes = lanes; self.bars = bars; self.ramp = ramp; self.trainer = trainer; self.rep = rep; self.end = end self._beat_ns = 60_000_000_000 // max(1, bpm); self._rebuild_dur_all() self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False self._dirty = False; self._overlay = None @@ -1240,28 +1256,50 @@ class App: try: self.midi.write(clk) except Exception: pass self._clock_next += tick_ns + def _end_plan(self): + # Per-track playback flow. Returns None to loop forever, else (fire_bars, action) where action is + # 'stop' or a signed int goto offset. Explicit end= governs; otherwise the global Continue toggle + # acts as a default end=next (legacy behaviour, still needs b to define the segment). + end = self.end + if end is None: + if self.continue_on and self.bars: end = 1 + else: return None + cyc = self.bars if self.bars else 1 # a cycle = b, else one master bar + reps = self.rep if self.rep else 1 + return (cyc * reps, end) + def _goto_target(self, offset): + items = self.setlists[self.sl]['items']; n = len(items) + t = self.idx + offset + return 0 if t < 0 else (t % n if t >= n else t) # before first -> clamp; past last -> wrap (loop) + def _end_stop(self): + self.running = False; self.spk.duty_cycle = 0; self.reset_playheads(); self._log_play() + self.led_rest(); self.draw_meters(); self._sync_broadcast("stop") def _on_new_bar(self, bar): - # Pre-parse the next track during the LAST bar of this segment, so the swap at the seam is allocation-free - if self.bars and self.continue_on and self._next_pending is None and bar == self.bars - 1: - self._prepare_next() - if self.bars and bar > 0 and bar % self.bars == 0: # segment boundary - self._seg_start = time.monotonic() # timer resets with the bar counter - if self.continue_on: - if self._next_pending is None: self._prepare_next() # late-toggled Continue: prep on the spot + plan = self._end_plan() # None = loop forever; else (fire_bars, action) + if plan is not None and plan[1] != 'stop' and self._next_pending is None and bar == plan[0] - 1: + self._prepare_next(self._goto_target(plan[1])) # pre-parse the target during the bar before the seam + if self.bars and bar > 0 and bar % self.bars == 0: # segment boundary -> reset the on-screen timer + self._seg_start = time.monotonic() + if plan is not None and bar > 0 and bar == plan[0]: # fire the end-action + action = plan[1] + if not (self.bars and bar % self.bars == 0): self._seg_start = time.monotonic() # no-bars: still reset the timer + if action == 'stop': + self._end_stop() + else: + if self._next_pending is None: self._prepare_next(self._goto_target(action)) # late prep if self._next_pending is not None: - self._seam_t = self.lanes[0]['next'] # the wall-clock time of THIS boundary step + self._seam_t = self.lanes[0]['next'] # wall-clock time of THIS boundary step self._advance = True # tick() will swap to the prepared track - # Note: per-master-step continuous ramp handles the bpm reset implicitly (seg_bar wraps to 0) t = self.trainer # gap trainer: silence during the rest bars self._muted = bool(t and (t['play'] + t['mute']) and (bar % (t['play'] + t['mute'])) >= t['play']) - def _prepare_next(self): # parse the next playlist item into a side holder + def _prepare_next(self, target=None): # parse a playlist item into a side holder for the gapless seam items = self.setlists[self.sl]['items'] - nxt = (self.idx + 1) % len(items) - if nxt == self.idx: return # 1-item playlist -> just loop, no swap + nxt = (self.idx + 1) % len(items) if target is None else target + if nxt == self.idx: return # same track (1-item list or self-goto) -> just loop, no swap name, prog = items[nxt] gc.collect() # defragment before parse_program allocates new lanes try: - bpm, lanes, bars, ramp, trainer = parse_program(prog) + bpm, lanes, bars, ramp, trainer, rep, end = parse_program(prog) except MemoryError: gc.collect(); return # leave _next_pending None -> the segment just loops beat = 60_000_000_000 // max(1, bpm) # pre-compute B's durs against B's bpm so the seam swap is allocation-free @@ -1276,13 +1314,14 @@ class App: else: d = beat // sub; L['durs'] = tuple(d for _ in range(steps)) self._next_pending = {'lanes': lanes, 'bpm': bpm, 'bars': bars, 'ramp': ramp, - 'trainer': trainer, 'name': name, 'idx': nxt} + 'trainer': trainer, 'name': name, 'idx': nxt, 'rep': rep, 'end': end} def _do_advance(self): # gapless seam: swap the prepared track in at seam_t n = self._next_pending if n is None: return self._next_pending = None self.lanes = n['lanes']; self.bpm = n['bpm']; self.bars = n['bars'] self.ramp = n['ramp']; self.trainer = n['trainer']; self.name = n['name']; self.idx = n['idx'] + self.rep = n['rep']; self.end = n['end'] # the swapped-in track's own playback flow governs from here self._beat_ns = 60_000_000_000 // max(1, self.bpm); self._rebuild_dur_all() # B's step grids built at the seam self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False; self._m_steps = 0 self._dirty = False; self._overlay = None diff --git a/pico-explorer/app.py b/pico-explorer/app.py index 8abc586..1b7039d 100644 --- a/pico-explorer/app.py +++ b/pico-explorer/app.py @@ -242,7 +242,7 @@ def _euclid(k, n, rot): # even distribution: k hi return [1 if ((((i + rot) % n) * k) % n) < k else 0 for i in range(n)] def parse_program(s): - bpm = 120; lanes = []; bars = 0; ramp = None; trainer = None + bpm = 120; lanes = []; bars = 0; ramp = None; trainer = None; rep = None; end = None for tok in s.strip().split(';'): tok = tok.strip() if not tok: continue @@ -262,11 +262,23 @@ def parse_program(s): try: trainer = {'play': max(0, int(p[0])), 'mute': max(0, int(p[1]))} except ValueError: pass continue + if tok.startswith('rep='): # rep= cycles before the end-action fires (playback flow) + try: rep = max(1, int(tok[4:])) + except ValueError: pass + continue + if tok.startswith('end='): # end=stop | end=next(+1) | end=<+/-N> relative goto; absent = loop forever + v = tok[4:] + if v == 'stop': end = 'stop' + elif v == 'next': end = 1 + else: + try: end = int(v) + except ValueError: pass + continue if ':' not in tok: continue lane = _parse_lane(tok) if lane: lanes.append(lane) if not lanes: lanes = [_parse_lane("beep:4")] - return max(5, min(300, bpm)), lanes, bars, ramp, trainer + return max(5, min(300, bpm)), lanes, bars, ramp, trainer, rep, end def _parse_lane(tok): poly = '~' in tok; mute = '!' in tok @@ -382,6 +394,7 @@ class App: self._chord_xz = 0 # 0 = not in chord; else monotonic_ns of the chord start self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.bars = 0 self.ramp = None; self.trainer = None; self._lastbar = -1; self._muted = False; self._ramp_base = 120 + self.rep = None; self.end = None # per-track playback flow: rep=cycles, end=stop|next|+/-N goto self._overlay = None # menu stack: None / 'menu' / 'settings' / 'help' / 'about' / 'log' / 'msg' self._modal_cursor = 0 # focused row in the current modal self._modal_rows = [] # tuples (label, value_str_or_None, action) for current modal @@ -494,7 +507,7 @@ class App: items = self.setlists[self.sl]['items'] self.idx = i % len(items) self.name, prog = items[self.idx] - self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog) + self.bpm, self.lanes, self.bars, self.ramp, self.trainer, self.rep, self.end = parse_program(prog) self._beat_ns = 60_000_000_000 // max(1, self.bpm); self._rebuild_dur_all() self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False self._overlay = None @@ -509,6 +522,9 @@ class App: if self.ramp: parts.append('rmp%d/%d/%d' % (self.ramp.get('start', self.bpm), self.ramp['amt'], self.ramp['every'])) if self.trainer: parts.append('tr%d/%d' % (self.trainer['play'], self.trainer['mute'])) for L in self.lanes: parts.append(lane_to_str(L)) + if self.end is not None: # per-track playback flow (default = loop forever -> omitted) + if self.rep and self.rep > 1: parts.append('rep=' + str(self.rep)) + parts.append('end=' + ('stop' if self.end == 'stop' else 'next' if self.end == 1 else ('+%d' % self.end if self.end > 0 else str(self.end)))) return ';'.join(parts) def toggle_continue(self): self.continue_on = not self.continue_on; self.draw_status() @@ -784,8 +800,8 @@ class App: try: cur = self._prog_str() except Exception: cur = None if patch and patch != cur: - bpm, lanes, bars, ramp, trainer = parse_program(patch) - self.bpm = bpm; self.lanes = lanes; self.bars = bars; self.ramp = ramp; self.trainer = trainer + bpm, lanes, bars, ramp, trainer, rep, end = parse_program(patch) + self.bpm = bpm; self.lanes = lanes; self.bars = bars; self.ramp = ramp; self.trainer = trainer; self.rep = rep; self.end = end self._beat_ns = 60_000_000_000 // max(1, bpm); self._rebuild_dur_all() self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False self._overlay = None @@ -969,26 +985,49 @@ class App: try: self.midi.write(clk) except Exception: pass self._clock_next += tick_ns + def _end_plan(self): + # Per-track playback flow. None = loop forever; else (fire_bars, action) where action is 'stop' or a + # signed int goto offset. Explicit end= governs; otherwise the global Continue toggle = default end=next. + end = self.end + if end is None: + if self.continue_on and self.bars: end = 1 + else: return None + cyc = self.bars if self.bars else 1 # a cycle = b, else one master bar + reps = self.rep if self.rep else 1 + return (cyc * reps, end) + def _goto_target(self, offset): + items = self.setlists[self.sl]['items']; n = len(items) + t = self.idx + offset + return 0 if t < 0 else (t % n if t >= n else t) # before first -> clamp; past last -> wrap (loop) + def _end_stop(self): + self.running = False; self.spk.duty_cycle = 0; self.reset_playheads(); self._log_play() + self._set_run_dot(); self.draw_meters(); self._sync_broadcast("stop") def _on_new_bar(self, bar): - if self.bars and self.continue_on and self._next_pending is None and bar == self.bars - 1: - self._prepare_next() - if self.bars and bar > 0 and bar % self.bars == 0: + plan = self._end_plan() # None = loop forever; else (fire_bars, action) + if plan is not None and plan[1] != 'stop' and self._next_pending is None and bar == plan[0] - 1: + self._prepare_next(self._goto_target(plan[1])) # pre-parse the target during the bar before the seam + if self.bars and bar > 0 and bar % self.bars == 0: # segment boundary -> reset the on-screen timer self._seg_start = time.monotonic() - if self.continue_on: - if self._next_pending is None: self._prepare_next() + if plan is not None and bar > 0 and bar == plan[0]: # fire the end-action + action = plan[1] + if not (self.bars and bar % self.bars == 0): self._seg_start = time.monotonic() # no-bars: still reset the timer + if action == 'stop': + self._end_stop() + else: + if self._next_pending is None: self._prepare_next(self._goto_target(action)) # late prep if self._next_pending is not None: - self._seam_t = self.lanes[0]['next'] - self._advance = True + self._seam_t = self.lanes[0]['next'] # wall-clock time of THIS boundary step + self._advance = True # tick() will swap to the prepared track t = self.trainer self._muted = bool(t and (t['play'] + t['mute']) and (bar % (t['play'] + t['mute'])) >= t['play']) - def _prepare_next(self): + def _prepare_next(self, target=None): items = self.setlists[self.sl]['items'] - nxt = (self.idx + 1) % len(items) + nxt = (self.idx + 1) % len(items) if target is None else target if nxt == self.idx: return name, prog = items[nxt] gc.collect() try: - bpm, lanes, bars, ramp, trainer = parse_program(prog) + bpm, lanes, bars, ramp, trainer, rep, end = parse_program(prog) except MemoryError: gc.collect(); return beat = 60_000_000_000 // max(1, bpm) @@ -1003,13 +1042,14 @@ class App: else: d = beat // sub; L['durs'] = tuple(d for _ in range(steps)) self._next_pending = {'lanes': lanes, 'bpm': bpm, 'bars': bars, 'ramp': ramp, - 'trainer': trainer, 'name': name, 'idx': nxt} + 'trainer': trainer, 'name': name, 'idx': nxt, 'rep': rep, 'end': end} def _do_advance(self): n = self._next_pending if n is None: return self._next_pending = None self.lanes = n['lanes']; self.bpm = n['bpm']; self.bars = n['bars'] self.ramp = n['ramp']; self.trainer = n['trainer']; self.name = n['name']; self.idx = n['idx'] + self.rep = n['rep']; self.end = n['end'] # the swapped-in track's own playback flow governs from here self._beat_ns = 60_000_000_000 // max(1, self.bpm); self._rebuild_dur_all() self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False; self._m_steps = 0 self._overlay = None diff --git a/pico/__pycache__/main.cpython-312.pyc b/pico/__pycache__/main.cpython-312.pyc deleted file mode 100644 index 2c1d997a90865e88810432c010069ae9559f5f62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72023 zcmeFa32`4V+LepHJp66KUq1c{@V6O%TgU*T|2vWnHk)|LtPJ>nO(chtgb>cyQ|2|>DpuFcI`Fuy7mpY%uPRVcRga}f7jKu z-{b1rZx)!F&78NrVkTXWa^4nmtGP|(J;r(4%^l`Wl~>GpyUd5ohgII=oVVL7G>cST z3FqxG_nP}u-V>bnh`HZFsn)xQm|!0lHkpNfB@B;Aq6#-r5xLHkH*z zyha_O)-xxI-I4TQSxhTnN2A=~027iX={!!HLXc&BKFEw+Y=6s)) z{yyl)bw8ORqUmDK{ZvY)QItth7DYJ}WmB|+qMe9}w-$S*^BTr3S%qVxh2e93h5f^0 zW7hQkaB11Wr_K-doFpDPqXUK3 zpxHP5Xt?yy!KWxmg(18Ck+TO-&)Ep7<+on4%)!3GbHU;M!qB-({r$szpC1p`l-ZU1 z>;ne|1`Z6I!lef*o(hkRoEV!p zjGQCXGIil-1KCeajE$K+!(*5@0iiie`s@L;wy8NdIyf*^_*kXCy0E8jq^GZVx;b20 ze(p;v@xVNbOr~*$BW1;^Eoz(h(H8wWhH+=|0wiltiq?`tf z|7}cAhK(A%BFN-5$*4jjkCl~_m6R7x7xNf-DLKb($GMW6&o$Ey`6~(^D=#T4o-PlU zmfEX7`s(iQFvi&c*;6A6+SmIy;;ws;;G)@;CNr?@S$&E zy1C^7x~%YA-^k$DaBzA@xTey%2nxI91fN${xF1dLFDx&-fKGaX)^wq6yIcX6eM@#z zLDe!lticPzePNJWI7GxP@%jMbE-&^(QjFf-zEMf1+$c5^I{tA$$XF8`bc%d+C1qVj~4Kz;gc8*Z5 zFIs;pYzaK)HI0U?vBD>LN+qv___|!og9@ET)oT)VMP{ z3VwYuJUG%Pnbz7$Vgz#t&}<89$_m3{g%BRok7$%V7+bj{ex78xQ}|fT!J6XfQb?Z) zo(UcyfIu-9IHl0dPnZ*-pZ>!DY;Nzd`hw?)+e<5d5pK_HMKi!|X=V6aXu1SEBqbBA zzeAkGVhQURNou)-wIOjBQ!8pI)q?ce+8c;;xk!@J^JhbduDZgAMX-D`o*CDyXVx|2 zG2PS92Bv2kkTY)6%TGRfnuh$Jahbkp$bL%Cdc&&q882!eAHPXdqu(PnX}ha9x%vKo zMn@JaiNB>(2BQ0ahlm)V{fyDteJT)1>X{1ng^GO@MBF_Q_jt$$phAStGcOGG zj`>GG$OkS=MUt(7o6%W;E3g7@=CD*QjqBFiy0c zZ!(O7$Bf#Frhs9bsx^%9TJ#k_*-4|C>a`km^{v!j-Gnh!NBy-`8z(1ZS(_{?m1WHX zvaDB@RmifIHd!`ytj;iw)wUT`wP>%l?Yz+%Xlgc$fp(+rU_0i09OEphA2?{#2AYi4 ziGwtjW~1IXQ$Im-4+QE@(cJ5ewsZAVcFL$f*+%26Hv;t%XQyO;^_})u0!L+8d!UKx zwHx*4n;6$d;CSu1juM)C`y`j4&50JC`#_+%f!Cqa=r||qRd;ZI6M?pRATNONoIZ7u z)~Lg1Xu`-ambO5LEPGz!v*C0*mkk8Y$T>VeIY49SG#UnEy{`Imb$)8|%sGR~+Kh&f zy|#f~xkhK(#!eeX+i|0zLblmoJ8{}*XbTygMyTB|PM$Ct>stxebrXTX`T($lv35=z zJZTu`n~lb{HejzFxx;l66Ih38qibLcy`s&=UMf3=vN2gN*fv0AO-AF`v4-*L`dTAk zahXwV^cb@2;92UgVIpv;-awnE(KAs^{k0lL4cX=-)uXcBfe@9QFpi#V=5e+K4%SXo z8-an4VV(jR8t2=Lqn%<71A+1eZnMqkGb*U8#W;Gtg~nMo5U8wcqd6Qj`X&v+3+hcY zflp|hHT41T1Lod;uIab|+#KsQPJzZ6>)X$i)f3I1s~;WVxldTme#%m_f9tZSMH(2BMSC)!#Z5*E-b4RNm!vv);{?DR5bxoM3 z*;fo!A)nR(CQyBhWt&yHHQ}e;EC~yI{%b-6z!H* z1Wx*^!4CD)Sib(6KtH+zg;79{Tt}=EdaMNn9~C$b^jEaCf|7gusB3VK<0WkYKVyb_ ztSV^=V*rG2^r+h{=as=6p^6YH%S$Hh5sCG;siNOs(^T8TAhcpUTYfs~*Q;zC_xmeo z<`j@7wyf)~nEt_NO_n$|l zR(~~G##pI=eqI}l8H(Ak{DjIn&>MbQ{gsFV7%P5`^7!pMNG2LNTAmKzpjT~n| zX7_;wX9;z}4{B-^SRP^#hki+r8ROg%))F|PzDSbU{T~M@B8+l_SV>ybCNWMsPfAO| zoL#p^%uUEg(MOXQk>3yTjc|fyC4U2x19-kTP*+|uRBd6Vpk03nX;<+p^8=*+Dt=$ zoYS$GNW$3Bo^GF3lDG_#8eyDxJRoSVRZtrh3i&+F>jkbOc~4YwoT;;gs4dV+QmVF} z$dz;o5iQX%bO(Zv3xGOaRWeDG3R*&_fhYtWb=Fl8CHDKPsHd6%5KuV_W|}f+lb9SB zfc%G6p2D9MmRM*h0=$0M5=@B?a)edYEM-rT!IvSn^-gwNsBL7$^mR%SGb2W_y zsk*)ze@U1Ygfj?V&=P}z7M2Vot)Zc-Vb&ubAS|zug0(@*LJLb2MFI1JBtw7>E1`8V zj1DP#(K9a^ftLtQ#cH)P5aclJDYzbw0jp(8O;AA~i1AGlWiVB8?Gpo&0X*@LNVUlk z(D(!{ktY;7!I8!R_51xW2<=q07+-yrgabVZ2?q#Eh$f^cFHm`Uz^Ej>VcQ!t{)W*O ze@)n4x1-=zYMJDnonlOLmfvreLeKPxQp-OE?4q;*9f{O9Cc~+^R%jBDqWRe=zX%6G zwRVczZjt@ktx#rc9%(rQ@SQVdOG)HCIOVVMPeESFx>%-T=K{t>g=feU=5c`Z3#lBH zqEfPLxJH}3s)WC~p=MB>pr8h8yy_UygwT~lP38VWgT` zB<;WRtcOFVrmoT595mH#kc^^ zC+Ji5Q6g7gXi5NpqQ&^J);0&AP2d@UW-2HQgeWzVQ_^nPP2nSg2jWLzQ3mYLHoD;7N%G+2A z{E0EPu*Jp{0-b5kh#Lv?6DLv#1lj7OA- zsB9Z(g~h?8*hBCF2cS(!mjud%WRSl^?9A`rGuAbNi$%-PF-nNDSPBRk)M4|3DnVhp z39SXs6NftNR}gGD(&(?j-t~D)=;8)jeb$vjRzSnrJ3PZAnP)+g^z@frENN>+RqQBD^DhJ)81;C!H0rw9;;?_fKoUcP} z1LitjGTG=adAA&&}Q3FOLI_&CFIRcSYd zFd}Qvs$8(OH`x#Nda$&&@d4Q^Z3AEsL9n9br%gGx11krzir0taDt3z0B9^KNEmAhb zVH@cga5a)F?JzIY5uuF)sR-31S&1f4^E`S67~4D8IdNYoErk-}5u>jOQL(Ip!0aYS zL$KC?U`0!SK#V#V2y1K58mcn{I>CA}M1#;>b{o_l&y_^F4FTmtX>J2(1vzjVA{M=1 za_Ea8z`9Tnb{iyV#Yn4cX>^=?D&%MrmNHW}sIb1iZm5do6QM0Y+i7O zSJ-7D6WElr=_uD4me3<*9^{|hK4}6l{1`n0w-mS+8ee(SNipj1C6*mx2GD8}Bc~x2 z1|^13yGEvIt>JSM)C8nV$TT!_wM+x~)onHmepK2C4!q8hsIca++!P z6j{{74Q#D(n|K(ThQs9o?PLWFVzi1L|31>ZW_Fv-G|m0*qy3n6Kzb|+n(X=fqR+PE z5|T>dNJz84*F4A@=4PV~`kd@zk~?+&nhT^tM1QbdAfc}7WC}B}@$bSg^F>H0czXUjIwP9)Rg58gV zr5#EQ$$9}xSfMo+?UK(KI$Oa?yxFk)sA2h0!jg5s)&}NJvO}gx!K*ZYRGKX*L2uKr zFJyiPw2w^V%xOTTF{mk(KJKqlY3MH6V-wzmbbpwdLqv?}$Z{r=DZ zrEpwFYh&-4`+%Od7NLnDO^BXZHiDOE#<0R*nvG(dRFQVh*dOw~n15MMqJT|h>C1ub z5)B1b8l&pyrj5K)JRWY}*`ooEPDMQ&2Etyj?PRPBf@AbFa@di`C;Fp+oiAqJWPsCP zjWb^~Ah!7d2$&M(gqNn+ayX+PEl{5V^(+#0`y?AFaQh;u_|Cy^@ymg|W98#jO|V1} z>@x}8FF0pvyV!l)3|51PjcoR?aSf5DY(#R8!RNiy22 z2^!KpVj-{#f^Qyd$imB}3}6(2MlUFWAIVIXyBHMW)Kshu751?QSm3VaJgu*dQIe)u zrzxWocte`8*TRy4af&vjRZMoX0|Hnhh=B09*lRsXo$(5Z$q@v6&a`{rUJ#Ld}2$bJ#;Br5@%aB_Ri2QSRfcoz+(}0;e(mL)d&RiM?SGbZEeSS-M}Rj$gh58 zw7JS(Nmvr2Lw1!)(W-(wsmFf?DS=gtU9KZ>`#)M5dobVtQV4Mm^D;1lGUAV##! zWdaU6w+cS#035nM4K5@yq5UPOkatiZSZFz6(t>)#`5;>|SbV@Dv|z!hCj^r6c9I&T zJuaApq!!u%)uO|4r0@(t%mp)=2DhUGqOXdfCeQ{-h=4?0$Q+bZ!C`)qu*R9FSk({v z2$*9Vvk5!>R!Aeh?EoKI=W##0;uUBfT|w54HRD{TwR(hg3eCD7$Na4|UBZIGilE&< zYl$g#g|MFKhCwU3Ze5 z8(M;dg2YD-_MNyKB}N3j)z-tS!;%mcdF81GiBGJfL^ydK*h)jQz#G|#6qx6PEQfAl ze=e^`HCCVK28gd7Kvj%U?EylpOi-dcoX#Yi=>5iMh{qb^hp6!M1h_| zoFZyv4;#;o?P|uNEv;z18ID(akds(NUQH%lR$0h~&1er8xF`{R1*|498v@YIRtu7H zBmhUI2ot=7hnQMGML2V(O8r6|3U2-f%OiRwD)pZiXR_ja*1p>z?&pm1%sSXxQa4VN zRy$7Ev+s(uGS*J8_0=eZhrrJ?+Y#dK$qA@y0CRgvT!l}N^x2XLw9G?$Zq0bvgC{urzw8KJZ*Ifn5^nJK8te{r(1fd`B_*Mj9JVOAtWK zz|SGtS+PDjP4!BR7Vc*pCb*Qi)hbnEmr^6Z6ZA9%YUP|U)DO}WEwdB@uKYcGckd$R zN4p3@HDnVll*PIZb`n+t`VOHFf&)r=LgdkUlMDkKz`tT14bq-7Fyk*_oRP5*KtTn0 zr^{LQ50DNjFJE6X!Es4NtUo&o8`55CK~*EejXYxoe1>ueO0tP4z4X3qY-!V973lc zD=Qi##Z3~eA0(@BipDV^gqq-L9wF|L;Y|XfTAmAJdoKY_i%|n(;8Y-s^Z{s=p@FoY z2O4^b{IO_<^b%S`H~x-B#wnVF9I|<%#+K(O|9jw#7M7;83k{JE!q(1EV9+k-4Y)*4 zLjHl+U>k>6L(@3O!X>RldsCev{Vhs$4c&@v8EUfy6>a+Xr_{&1rkDo;uOFubUO5@J z-1sNglQcG_U>>BP3r;>j&trbI&3>X{tmy>N78wzc#IzR$ebL&>ncKLqXaC^bTJr2- z+empDQ-lu5QgSR0Wc#lZZD-dGY25S%KVQnbx zRFIUw{ucWl;27__e8qBL2Q0z$lzt(DM2Wq+~{d7q5FQgBhM#3)IZ;5 zg`6p)MZzs%I>biroj@Ytmu|dbZv~tTfu+Q|Bq#wG`O@(-CcGZBzZXtR(x%)Qdk1*k z8IZhGXbLzOg^Nnkl@EN?oG5Gt#Z1Ur!6+~cp(n(WGn!i1E`!#> zE#_ALLCmL8*lMtrN`>i(@#2n$t+xoZ2xyV_s=UkR9AI52GH5$C;%NsmiTf9H+pnBv z7Q!Ep0sFBJJ)sg144fj1ZWY7gV{Z{b_KH<#MuX4{FjwIX!sxIrz{{jyG1xico;*!a zAfD(IdP7hh$xOg2`NJnKl!gB3hxZ!{1HB+7g%vn2bEr|m2nDfG07jt~*q$Z2pz}Q5 z$73bgXaL2-1n2gdL&1&05=0APEZi7cv+ZkY1dd?r^Eg3tOI9WV`YjcCpeivoJn}7U z&c>|lVw6|e$$;7!V)1!I>^ZU<2Ucx51yP_}bWW^J@`8gsKcu7D7qdrhl}+AJBw%5l9BPklg>a%`FIa4=7eo_0-01QVPOWGqK(E8YZ6^#EP?SW!K z!}xe*`N_6pfC6vYA?E#t@aeW=UD#Q`=OQmS#L}a*8S5 zP60xu{52}&_n(n-KpQv@vID5S(dG~C1t&}tNL;|zK}nk*7)q`ppcq0xDUN{ySuQzB z>dsKH2>y~znM7}gY$&X|y4gla8_FIJK`{iCjL>@9p`=re7&VBCIh6ED2&8PDr<7=e zQ{0A~0)$K%bt;8pT{%wr9c+^S!8A}~VF~sBzo*)&OY%R6#Un?8&z*9)@B;Eg^H9jB zo9;ADc67qkW0VQLpJm~fD}fb`}f`5p07U z)nLfj!A2t-O(HmOuVBaphM=%xKT}>ybUlp?H3>HD1(Z;BY!GvXV+qw5g~B7VQ3Pop zpf5dn&V)H=hcOz72r$#^Fi9C&7)h0i%mxV}4zo0jLZK6{JV@QOLk#MQtUB#o3L1CN zxLyoa|5`r(+%RmJk!c}9+;$k|3;OypFg#{GFUQ(Htb1Jg z5pGXH8`F=bLq>(5AF{1P;O8$x&knxO#Hx^Q^V65%FU127;8V@-sxQ4160b^b2$K#o z(d%Q&4L-)>iPngTldIbfigh*B+pD2DM@b}yJY@N&F(!>Ow69jopHQwkxLc0H{&mP_ zY~|tx+h7$l`7-(OaHY9$DpI_o<15wx+zodN_-?^nYQ zk$gkrV5qzgZGZ0KHlaQ)ORdF>oBLf-=l` zUgxE8@7%v?_c35fPC2G&AI5QGD%+}zUT3{?&}h_m@a<}9JYv_R8gnLE6cF#aokc1Q z2elm%!|2^t6n&^RD4}6AMxp@lp!$+mU}6FAtaoS$`_g+EhsPtPDf?&!Q%l$kAE=1A z$8wNr3|E-!pmtxXOeWN<>?>q4-|?$gG-fa3fKjHVy1?fs6vjl99wGWi@wqu5pCnHOK#mU*$iO0fR2omkZH z^H;(TuSrMn*myI&5>(ZD=`22&85?TPXlWt3tQm+Ag<6g}##9r{KXGBy4NuW&0vN5l z0G0^GNtB6T>?2U_EFlbErho8w^Z{<-@i^T4V$3xc8%~~sop4l3A$7(m=2h9Lv0tq* zsgBTbnN!*kVtndE!M@HXa-(CdzY^<8Eu|B~Xnm@^m(e}pN$K2#^3?&8P|}DOQ5XT@ za>a&zkfG=0^dCk;YWXR>qS57eQfgTp&qspJ2c_b6sY?Zpj#I&kUSmb9*R6jbQ8&aNjU1+DY6J1?!*#LtpA9m12{{|o>7aqVo zdIz+jy2|)6FsiSOOQzb9LwG8|8**S?^@x$rb_Q*crr;KMdn+CJ{^&uHuhJGbo`TK1?*4;=^dZO4yF(8OC

#zVe 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 diff --git a/src/engine.js b/src/engine.js index 7f9b637..81b9a39 100644 --- a/src/engine.js +++ b/src/engine.js @@ -226,18 +226,24 @@ function setupToPatch(s) { (s.lanes || []).forEach((c) => parts.push(laneCfgToStr(c))); if (s.trainer && s.trainer.on) parts.push("tr" + s.trainer.playBars + "/" + s.trainer.muteBars); if (s.ramp && s.ramp.on) parts.push("rmp" + s.ramp.startBpm + "/" + s.ramp.amount + "/" + s.ramp.everyBars); + if (s.end != null) { // per-track playback flow (default = loop forever) + if (s.rep != null && s.rep > 1) parts.push("rep=" + s.rep); // cycles before end fires (1 = default, omitted) + parts.push("end=" + (s.end === "stop" ? "stop" : s.end === 1 ? "next" : s.end > 0 ? "+" + s.end : String(s.end))); + } return parts.join(";"); } function patchToSetup(str) { - const s = { bpm: 120, volume: null, countMs: 0, bars: 0, lanes: [], trainer: { on: false, playBars: 2, muteBars: 2 }, ramp: { on: false, startBpm: 80, amount: 5, everyBars: 4 } }; + const s = { bpm: 120, volume: null, countMs: 0, bars: 0, lanes: [], rep: null, end: null, trainer: { on: false, playBars: 2, muteBars: 2 }, ramp: { on: false, startBpm: 80, amount: 5, everyBars: 4 } }; for (let tok of String(str).split(";")) { tok = tok.trim(); if (!tok || tok === "v1") continue; if (tok.includes(":")) { const c = laneStrToCfg(tok); if (c) s.lanes.push(c); } // lanes contain ":" → matched first else if (tok.startsWith("vol")) s.volume = (parseInt(tok.slice(3), 10) || 0) / 100; else if (tok.startsWith("cd")) s.countMs = (parseInt(tok.slice(2), 10) || 0) * 1000; - else if (tok.startsWith("b")) s.bars = parseInt(tok.slice(1), 10) || 0; // segment bar-length + else if (tok.startsWith("rep=")) s.rep = parseInt(tok.slice(4), 10) || 1; // playback flow: cycles before end fires + else if (tok.startsWith("end=")) { const v = tok.slice(4); s.end = v === "stop" ? "stop" : v === "next" ? 1 : (parseInt(v, 10) || 0); } // stop | next(+1) | relative goto ±N 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("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; } return s; diff --git a/tests/adapters/js_adapter.mjs b/tests/adapters/js_adapter.mjs index 2c70e67..31e3be3 100644 --- a/tests/adapters/js_adapter.mjs +++ b/tests/adapters/js_adapter.mjs @@ -25,8 +25,8 @@ export function normalize(patch) { countMs: s.countMs || 0, ramp: s.ramp && s.ramp.on ? { start: s.ramp.startBpm, amt: s.ramp.amount, every: s.ramp.everyBars } : null, trainer: s.trainer && s.trainer.on ? { play: s.trainer.playBars, mute: s.trainer.muteBars } : null, - rep: s.rep == null ? null : s.rep, // not parsed yet → undefined → null - end: s.end == null ? null : s.end, // not parsed yet → undefined → null + end: s.end == null ? null : s.end, + rep: s.end == null ? null : (s.rep == null ? 1 : s.rep), // rep only meaningful with end; defaults to 1 lanes: (s.lanes || []).map((c) => ({ sound: c.sound, groups: groupsArr(c.groupsStr), diff --git a/tests/adapters/py_adapter.py b/tests/adapters/py_adapter.py index 8917fc1..56d892d 100644 --- a/tests/adapters/py_adapter.py +++ b/tests/adapters/py_adapter.py @@ -31,8 +31,8 @@ NS = {} exec("\n".join(_segs), NS) -def _prog_str(bpm, lanes, bars, ramp, trainer): - # Mirrors app.py App._prog_str (app.py:577) using the real lane_to_str. +def _prog_str(bpm, lanes, bars, ramp, trainer, rep=None, end=None): + # Mirrors app.py App._prog_str using the real lane_to_str. parts = ["t" + str(bpm)] if bars: parts.append("b" + str(bars)) @@ -42,6 +42,10 @@ def _prog_str(bpm, lanes, bars, ramp, trainer): parts.append("tr%d/%d" % (trainer["play"], trainer["mute"])) for L in lanes: parts.append(NS["lane_to_str"](L)) + if end is not None: + if rep and rep > 1: + parts.append("rep=" + str(rep)) + parts.append("end=" + ("stop" if end == "stop" else "next" if end == 1 else ("+%d" % end if end > 0 else str(end)))) return ";".join(parts) @@ -55,7 +59,7 @@ def _gain_db(g): def normalize(patch): - bpm, lanes, bars, ramp, trainer = NS["parse_program"](patch) + bpm, lanes, bars, ramp, trainer, rep, end = NS["parse_program"](patch) return { "bpm": bpm, "bars": bars, @@ -63,8 +67,8 @@ def normalize(patch): "countMs": 0, # device has no count-in "ramp": {"start": ramp.get("start", bpm), "amt": ramp["amt"], "every": ramp["every"]} if ramp else None, "trainer": {"play": trainer["play"], "mute": trainer["mute"]} if trainer else None, - "rep": None, # not parsed yet - "end": None, # not parsed yet + "rep": None if end is None else (rep if rep else 1), # rep only meaningful with end; defaults to 1 + "end": end, "lanes": [ { "sound": L["sound"], diff --git a/tests/fixtures/track-format.json b/tests/fixtures/track-format.json index 6cc0069..1151411 100644 --- a/tests/fixtures/track-format.json +++ b/tests/fixtures/track-format.json @@ -5,183 +5,655 @@ "id": "minimal", "status": "stable", "in": "t120;kick:4=Xxxx", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "backbeat", "status": "stable", "in": "t120;kick:4=Xxxx;snare:4=.x.x;hatClosed:4/2=X.x.x.x.", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, "lanes": [ - { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] }, - { "sound": "snare", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [0,1,0,1] }, - { "sound": "hatClosed", "groups": [4], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0,1,0,1,0] } - ] } + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + }, + { + "sound": "snare", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [0, 1, 0, 1] + }, + { + "sound": "hatClosed", + "groups": [4], + "sub": 2, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 0, 1, 0, 1, 0, 1, 0] + } + ] + } }, { "id": "odd-meter-2+2+3", "status": "stable", "in": "t130;kick:2+2+3=x..x..x;hatClosed:2+2+3/2=X.x.X.x.X.x.x.", - "norm": { "bpm": 130, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, + "norm": { + "bpm": 130, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, "lanes": [ - { "sound": "kick", "groups": [2,2,3], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [1,0,0,1,0,0,1] }, - { "sound": "hatClosed", "groups": [2,2,3], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0,2,0,1,0,2,0,1,0,1,0] } - ] } + { + "sound": "kick", + "groups": [2, 2, 3], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [1, 0, 0, 1, 0, 0, 1] + }, + { + "sound": "hatClosed", + "groups": [2, 2, 3], + "sub": 2, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 0, 1, 0, 2, 0, 1, 0, 2, 0, 1, 0, 1, 0] + } + ] + } }, { "id": "swing", "status": "stable", "in": "t150;ride:4/2s=X.x.x.x.;kick:4=X..x", - "norm": { "bpm": 150, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, + "norm": { + "bpm": 150, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, "lanes": [ - { "sound": "ride", "groups": [4], "sub": 2, "swing": true, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0,1,0,1,0] }, - { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,0,1] } - ] } + { + "sound": "ride", + "groups": [4], + "sub": 2, + "swing": true, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 0, 1, 0, 1, 0, 1, 0] + }, + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 0, 0, 1] + } + ] + } }, { "id": "ghost-notes", "status": "stable", "in": "t92;snare:4/3=..gg.gX.gg.g", - "norm": { "bpm": 92, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "snare", "groups": [4], "sub": 3, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [0,0,3,3,0,3,2,0,3,3,0,3] } ] } + "norm": { + "bpm": 92, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "snare", + "groups": [4], + "sub": 3, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [0, 0, 3, 3, 0, 3, 2, 0, 3, 3, 0, 3] + } + ] + } }, { "id": "polymeter", "status": "stable", "in": "t100;kick:4=Xxxx;claves:5=Xxxxx~", - "norm": { "bpm": 100, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, + "norm": { + "bpm": 100, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, "lanes": [ - { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] }, - { "sound": "claves", "groups": [5], "sub": 1, "swing": false, "poly": true, "mute": false, "gainDb": 0, "levels": [2,1,1,1,1] } - ] } + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + }, + { + "sound": "claves", + "groups": [5], + "sub": 1, + "swing": false, + "poly": true, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1, 1] + } + ] + } }, { "id": "disabled-lane", "status": "stable", "in": "t120;kick:4=Xxxx;hatClosed:4=Xxxx!", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, "lanes": [ - { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] }, - { "sound": "hatClosed", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": true, "gainDb": 0, "levels": [2,1,1,1] } - ] } + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + }, + { + "sound": "hatClosed", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": true, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "ramp", "status": "stable", "in": "t80;woodblock:4=Xxxx;rmp80/4/4", - "norm": { "bpm": 80, "bars": 0, "volume": null, "countMs": 0, "ramp": { "start": 80, "amt": 4, "every": 4 }, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "woodblock", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 80, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": { + "start": 80, + "amt": 4, + "every": 4 + }, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "woodblock", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "gap-trainer", "status": "stable", "in": "t100;kick:4=Xxxx;tr2/2", - "norm": { "bpm": 100, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": { "play": 2, "mute": 2 }, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 100, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": { + "play": 2, + "mute": 2 + }, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "segment-bars", "status": "stable", "in": "t88;b8;kick:4=X.x.", - "norm": { "bpm": 88, "bars": 8, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,1,0] } ] } + "norm": { + "bpm": 88, + "bars": 8, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 0, 1, 0] + } + ] + } }, { "id": "gm-note-number-alias", "status": "stable", "in": "t120;36:4=Xxxx", "note": "GM note 36 = kick. Both engines resolve the number to the voice name.", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, - { "id": "default-pattern", "status": "stable", "note": "No =pattern: every subdivision sounds at normal; accent only on group starts (the grouping is the accent map). One group of 4 ⇒ accent on beat 1 only, off-8ths sound at normal.", "in": "t120;hatClosed:4/2", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "hatClosed", "groups": [4], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1,1,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "hatClosed", + "groups": [4], + "sub": 2, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1, 1, 1, 1, 1] + } + ] + } }, { "id": "default-pattern-odd-meter", "status": "stable", "note": "No =pattern over a 2+2+3 grouping ⇒ accents land on group starts (beats 1,3,5); all 8ths sound.", "in": "t130;hatClosed:2+2+3/2", - "norm": { "bpm": 130, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "hatClosed", "groups": [2,2,3], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1,2,1,1,1,2,1,1,1,1,1] } ] } + "norm": { + "bpm": 130, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "hatClosed", + "groups": [2, 2, 3], + "sub": 2, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1] + } + ] + } }, { "id": "euclid", "status": "stable", "note": "Euclid (k,n) shorthand: 3 hits spread over 8 steps, first accented. Parsed identically on both engines.", "in": "t120;kick:4(3,8)", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 2, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,0,0,1,0,0,1,0] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 2, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 0, 0, 1, 0, 0, 1, 0] + } + ] + } }, { "id": "vol-and-countin", "status": "divergence", - "expectFail": ["py"], + "expectFail": [ + "py" + ], "note": "INTENTIONAL host difference (permanent, not a bug): vol (master volume) and cd (count-in) are web-authoring fields. The device has a hardware volume knob and no count-in, so it omits them. Kept as a vector to document the boundary.", "in": "t120;vol80;cd2;kick:4=Xxxx", - "norm": { "bpm": 120, "bars": 0, "volume": 0.8, "countMs": 2000, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": 0.8, + "countMs": 2000, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "unknown-sound", "status": "stable", "note": "Unknown sound name falls back to beep on both engines.", "in": "t120;blorp:4=Xxxx", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "beep", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "beep", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "tempo-clamp-high", "status": "divergence", - "expectFail": ["js"], + "expectFail": [ + "js" + ], "note": "Firmware clamps t to [5,300]; engine.js does not. Spec = clamp everywhere.", "in": "t999;kick:4=Xxxx", - "norm": { "bpm": 300, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 300, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "empty-defaults-to-beep", "status": "divergence", - "expectFail": ["js"], + "expectFail": [ + "js" + ], "note": "Firmware injects a default beep:4; engine.js returns no lanes (editor guards emptiness). Spec = beep:4.", "in": "", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": null, "end": null, - "lanes": [ { "sound": "beep", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": null, + "end": null, + "lanes": [ + { + "sound": "beep", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, - { "id": "end-stop", - "status": "new", - "expectFail": ["js", "py"], + "status": "stable", "note": "Per-track playback flow — not yet implemented anywhere.", "in": "t120;kick:4=Xxxx;end=stop", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": 1, "end": "stop", - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": 1, + "end": "stop", + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "rep-then-next", - "status": "new", - "expectFail": ["js", "py"], + "status": "stable", "note": "Play 4 cycles then auto-advance. next ⇒ +1.", "in": "t120;kick:4=Xxxx;rep=4;end=next", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": 4, "end": 1, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": 4, + "end": 1, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } }, { "id": "relative-goto-back-two", - "status": "new", - "expectFail": ["js", "py"], + "status": "stable", "note": "Relative goto (D.S.): after 1 cycle, jump back two tracks.", "in": "t120;kick:4=Xxxx;end=-2", - "norm": { "bpm": 120, "bars": 0, "volume": null, "countMs": 0, "ramp": null, "trainer": null, "rep": 1, "end": -2, - "lanes": [ { "sound": "kick", "groups": [4], "sub": 1, "swing": false, "poly": false, "mute": false, "gainDb": 0, "levels": [2,1,1,1] } ] } + "norm": { + "bpm": 120, + "bars": 0, + "volume": null, + "countMs": 0, + "ramp": null, + "trainer": null, + "rep": 1, + "end": -2, + "lanes": [ + { + "sound": "kick", + "groups": [4], + "sub": 1, + "swing": false, + "poly": false, + "mute": false, + "gainDb": 0, + "levels": [2, 1, 1, 1] + } + ] + } } ] } diff --git a/wokwi/__pycache__/ssd1306.cpython-312.pyc b/wokwi/__pycache__/ssd1306.cpython-312.pyc deleted file mode 100644 index ca231785b638e6be98ab53afd61c45dcd430b509..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5509 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