From 2d243c9ef8bf9baa5005205a9d32cf58c72f564b Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 12:29:09 -0500 Subject: [PATCH] PM_K-1 0.0.8: built-in playlists (baked, read-only) vs user playlists (separate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standard editor defaults (Styles / Practice / Song) are baked into app.py as BUILTIN_SETLISTS (ASCII-fied — emoji/accents would break the 7-bit push + the fonts), so they update with firmware and the user can't change or delete them. User playlists live separately in programs.json and are merged after the built-ins. Device: - Set-list model: self.setlists = built-ins + user lists (deduped by normalized title, so a baked built-in always wins). load()/goto() work within the current list. - Navigation: a set-list "tab" (small, above the title) shows playlist + position, muted for built-in / cyan for user; TAP it to switch playlists. Joystick L/R = item. - SysEx 0x10 (push) writes programs.json -> rebuild user lists; built-ins untouched. - Shipped programs.json is now empty ({"setlists":[]}) — built-ins come from firmware. Editor: - "Save to device" now syncs only YOUR set lists (filters out the built-in seeds) in the new {setlists:[...]} format; warns if you have none. Load-from-device imports both the new multi-list and old flat formats. Verified in the harness: 3 read-only built-ins, set-list switching, user-list merge + dedup of a pushed "styles", and the ramp engine on a built-in track (80->84->88, +4/4 bars). Co-Authored-By: Claude Opus 4.7 (1M context) --- editor.html | 35 +++++-- pico-cp/__pycache__/app.cpython-312.pyc | Bin 61045 -> 65560 bytes pico-cp/app.py | 121 ++++++++++++++++++------ pico-cp/programs.json | 96 +------------------ 4 files changed, 116 insertions(+), 136 deletions(-) diff --git a/editor.html b/editor.html index 724271f..95f32a6 100644 --- a/editor.html +++ b/editor.html @@ -347,7 +347,7 @@ - + @@ -1075,9 +1075,16 @@ function shareSetlist() { /* Device (PM_K-1) programs.json — the same grooves the firmware reads. Save: writes the active set list straight onto the CIRCUITPY drive (File System Access, Chrome/Edge) or downloads it to drag on. Load: reads a programs.json into a new set list. */ +// The PM_K-1's built-in playlists are baked into its firmware (they update with firmware and are +// read-only). "Save to device" syncs only YOUR set lists — i.e. the ones that aren't the built-in demos. +function userSetlists() { + const seed = new Set(SEED_SETLISTS.map((s) => s.title)); + return setlists.filter((sl) => !seed.has(sl.title)); +} function programsJSON() { - const sl = getSL(); if (!sl) return null; - return JSON.stringify({ title: sl.title || "PolyMeter", programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) }, null, 2); + return JSON.stringify({ setlists: userSetlists().map((sl) => ({ + title: sl.title || "My set list", + programs: sl.items.map((it) => ({ name: it.name, prog: setupToPatch(it) })) })) }, null, 2); } function _downloadPrograms(json) { const a = document.createElement("a"); @@ -1085,7 +1092,9 @@ function _downloadPrograms(json) { a.download = "programs.json"; a.click(); URL.revokeObjectURL(a.href); } async function saveToDevice() { - const json = programsJSON(); if (!json) return alert("No set list selected to save."); + const uls = userSetlists(); + if (!uls.length) return alert("No custom set lists to save.\n\nThe built-in playlists (Styles / Practice / Song) are baked into the device firmware — they're always there, update with firmware, and can't be changed. Create your own set list and it'll save here."); + const json = programsJSON(); // Primary: push to the device over USB-MIDI SysEx (Chromium/Firefox); the firmware writes programs.json. if (await _ensureMidi() && _midiOutputs().length) { const ascii = json.replace(/[\u0080-\uFFFF]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")); // 7-bit-safe JSON @@ -1094,7 +1103,7 @@ async function saveToDevice() { bytes.push(0xF7); _send(_clockSysex()); _send(bytes); const ok = await new Promise((res) => { _saveCb = res; setTimeout(() => { if (_saveCb) { _saveCb = null; res(null); } }, 2500); }); - if (ok === true) { alert("Saved to device ✓ — it reloaded with your set list."); return; } + if (ok === true) { alert("Saved to device ✓ — " + uls.length + " set list(s) synced (alongside the built-ins)."); return; } _downloadPrograms(json); alert(ok === false ? "The device is in editor mode (drive writable by the computer), so I downloaded programs.json — drag it onto the CIRCUITPY drive." @@ -1108,11 +1117,17 @@ async function saveToDevice() { function importPrograms(text) { try { const d = JSON.parse(text); - const items = (d.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) })); - if (!items.length) return alert("No programs found in that file."); - setlists.push({ title: d.title || "Device", description: "", items }); activeSL = setlists.length - 1; activeItem = 0; - saveSetlists(); renderSetlists(); applySetup(items[0]); - alert("Loaded " + items.length + " grooves from the device into a new set list."); + const lists = Array.isArray(d.setlists) ? d.setlists : [{ title: d.title, programs: d.programs || [] }]; + let added = 0; + for (const sl of lists) { + const items = (sl.programs || []).map((p) => ({ name: p.name || "Item", ...patchToSetup(p.prog) })); + if (!items.length) continue; + setlists.push({ title: sl.title || "Device", description: "", items }); added++; + } + if (!added) return alert("No programs found in that file."); + activeSL = setlists.length - 1; activeItem = 0; + saveSetlists(); renderSetlists(); applySetup(setlists[activeSL].items[0]); + alert("Loaded " + added + " set list(s) from the device."); } catch (e) { alert("Load failed: " + e.message); } } async function loadFromDevice() { diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index 779707f61f7d1a17c55060517a10dff8f33c8760..84c350f15dbaf49c7bbccdc6e745012c83622753 100644 GIT binary patch delta 17621 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 I^Lc>(7oeA%H2?qr delta 13861 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 len(self.setlists): self.sl = 0 + def switch_setlist(self, delta=1): + if len(self.setlists) < 2: return + was = self.running + if was: self.running = False; self._log_play() + self.sl = (self.sl + delta) % len(self.setlists) + self.load(0) + if was: self.running = True; self._reset_clock(); self._start_play() + self.led_rest(); self.draw_meters() def load(self, i): - n = len(self.programs); self.idx = i % n - self.name, prog = self.programs[self.idx] + 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.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train() @@ -549,7 +605,9 @@ class App: if pt: self._touchSeen = nowms if not self._touchDown: - self._touchDown = True; self._tap_log(pt[0], pt[1]) + self._touchDown = True + if 112 <= pt[1] <= 154: self.switch_setlist(1) # tap the set-list tab/title -> next playlist + else: self._tap_log(pt[0], pt[1]) elif self._touchDown and (nowms - self._touchSeen) > 0.14: self._touchDown = False # USB-MIDI in: any byte = a host is listening (heartbeat); also assemble SysEx (clock / pushed programs) @@ -571,14 +629,16 @@ class App: # ---------- drawing ---------- def draw_bpm(self): self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12) - def draw_status(self): # title (bright) + track number set apart (dim, right) - self._place(self.g_name, self.name[:22], 12, 130, C_TXT, C_BG, FONT_M) - self._place(self.g_idx, "%d/%d" % (self.idx + 1, len(self.programs)), 0, 134, - C_DIM, C_BG, FONT_S, right_edge=WIDTH - 12) + def draw_status(self): # set-list tab (small, tap to switch) above the item title + sl = self.setlists[self.sl] + # tab: playlist + position; muted = built-in (read-only), cyan = your own + self._place(self.g_idx, "%s %d/%d" % (sl['title'][:14], self.idx + 1, len(sl['items'])), + 12, 118, C_MUTE if sl['builtin'] else C_CYAN, C_BG, FONT_S) + self._place(self.g_name, self.name[:22], 12, 134, C_TXT, C_BG, FONT_M) def draw_train(self): # ramp + gap-trainer indicators (symbol + params), when set g = self.g_train while len(g): g.pop() - x = 12; y = 104 + x = 12; y = 100 if self.ramp: up = self.ramp['amt'] >= 0 pts = [(0, 9), (12, 9), (12, 0)] if up else [(0, 0), (0, 9), (12, 9)] # rising / falling ramp @@ -739,14 +799,13 @@ class App: except Exception: pass elif cmd == 0x02: # version query -> reply 0x03 + APP_VERSION if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x03]) + APP_VERSION.encode() + bytes([0xF7])) - elif cmd == 0x10: # write /programs.json pushed from the editor, then reload + elif cmd == 0x10: # write /programs.json (user playlists) pushed from the editor try: with open("/programs.json", "wb") as f: f.write(bytes(sx[2:])) - self.programs = load_programs(); self.idx = min(self.idx, len(self.programs) - 1) - self.load(self.idx) - if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7F, 0xF7])) # ACK ok - except OSError: - if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x7E, 0xF7])) # NAK: read-only (editor mode) + self.rebuild_setlists(); self.load(0) # built-ins untouched; show the refreshed lists + self._ack(True) + except Exception: + self._ack(False) # read-only (editor mode) etc. # A/B firmware update, sent as small flow-controlled chunks (a single huge SysEx overruns the # USB-MIDI input buffer and arrives corrupt). begin(0x21,len) -> data(0x22)* -> commit(0x23). elif cmd == 0x21: # BEGIN: open the staging file; sx[2:6] = expected length (7-bit) diff --git a/pico-cp/programs.json b/pico-cp/programs.json index dd42d4d..2a81802 100644 --- a/pico-cp/programs.json +++ b/pico-cp/programs.json @@ -1,97 +1,3 @@ { - "title": "PolyMeter", - "programs": [ - { - "name": "Four on the floor", - "prog": "t120;kick:4;snare:4=.x.x;hatClosed:4/2" - }, - { - "name": "Swing ride", - "prog": "t150;ride:4/2s;kick:4=X..x;snare:4=.x.x" - }, - { - "name": "Purdie half shuffle", - "prog": "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x" - }, - { - "name": "Samba (2/4)", - "prog": "t104;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX." - }, - { - "name": "Nanigo (6/8 bembe)", - "prog": "t130;cowbell:4/3=X.xx.x.xx.x.;kick:4/3=X.....X.....;hatClosed:4/3=..x..x..x..x" - }, - { - "name": "6/8 groove", - "prog": "t100;kick:3+3=x..x..;snare:3+3=...x..;hatClosed:3+3/2" - }, - { - "name": "7/8 (2+2+3)", - "prog": "t130;kick:2+2+3=x..x..x;hatClosed:2+2+3/2" - }, - { - "name": "5/4 (3+2)", - "prog": "t112;kick:3+2=x..x.;snare:3+2=..x..;hatClosed:3+2/2" - }, - { - "name": "5 over 4 polyrhythm", - "prog": "t100;kick:4;claves:5~" - }, - { - "name": "3 over 2 hemiola", - "prog": "t96;woodblock:2;cowbell:3~" - }, - { - "name": "2 & 4 & 3 per bar", - "prog": "t100;kick:3;cowbell:2~;claves:4~" - }, - { - "name": "Triplet hats", - "prog": "t100;kick:4;snare:4=.x.x;hatClosed:4/3" - }, - { - "name": "Accents", - "prog": "t92;kick:4=X..X;snare:4=.X.X;hatClosed:4/2" - }, - { - "name": "Tempo builder 80+", - "prog": "t80;woodblock:4;rmp80/4/4" - }, - { - "name": "Gap trainer 2/2", - "prog": "t100;kick:4;hatClosed:4/2;tr2/2" - }, - { - "name": "Intro - hats+kick", - "prog": "t88;kick:4=X.x.;hatClosed:4/2=gggggggg" - }, - { - "name": "Groove in", - "prog": "t88;kick:4=X.x.;snare:4=.X.X;hatClosed:4/2" - }, - { - "name": "Half-time shuffle", - "prog": "t92;kick:4/3=X....x...x..;snare:4/3=..gg.gX.gg.g;hatClosed:4/3=X.xX.xX.xX.x" - }, - { - "name": "Build 92 to 120", - "prog": "t92;kick:4;snare:4=.X.X;hatClosed:4/2" - }, - { - "name": "Four-floor (909)", - "prog": "t124;kick909:4;clap909:4=.X.X;hat909:4/2=.X.X.X.X" - }, - { - "name": "Samba break", - "prog": "t116;tomLow:2/4=x...X...;hatClosed:2/4;woodblock:2/4=X.xx.xX." - }, - { - "name": "Peak - 16ths", - "prog": "t132;kick:4=X..x;snare:4=.X.X;hatClosed:4/4" - }, - { - "name": "Outro", - "prog": "t132;kick:4=X..x;hatClosed:4/2=gggggggg" - } - ] + "setlists": [] }