From dbc9fa7fdc1e84071e5c719cfb26791057c656f3 Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 14:44:04 -0500 Subject: [PATCH] PM_K-1 0.1.0: on-device lane editor - edit/add/remove lanes Tap the instrument name -> a modal to change Sound (cycle the GM voices), Beats (1-12), Subdivision (1-8), Swing, and Mute, plus + Lane / Remove (1..MAXLANES). Beats/sub changes regenerate the lane's default accents; sound/swing/mute keep the pattern. Reuses the existing dirty + Save/Revert + .mpy machinery (edits to a built-in save a copy to "My edits"). The modal redraws live as you adjust; tap Done or outside to close. Verified in harness: editor opens (13 hit-zones), sound cycles, beats/sub regen steps, swing/mute toggle, add/remove lanes, the edited track serializes + round-trips, Done closes; modal renders cleanly. app.mpy builds (C/v6). This completes the Phase-2 editing set (beats + lanes + Continue + built-in/user split). Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/README.md | 7 +- pico-cp/__pycache__/app.cpython-312.pyc | Bin 79878 -> 89940 bytes pico-cp/app.py | 84 ++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 9 deletions(-) diff --git a/pico-cp/README.md b/pico-cp/README.md index a532f3c..6a48cd1 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -67,9 +67,10 @@ The editor also syncs the device clock, so the practice log gets real wall‑clo firmware. **Your own** playlists live in `programs.json` (synced from the editor's *Save to device*). - **Switch playlist:** tap the **set-list tab** (above the title; grey = built-in, cyan = yours). **Item:** joystick left/right. -- **Edit on the device:** **tap a beat** to cycle it (off → normal → accent → ghost). The title turns - **red** (unsaved); **tap the title** to **Save** or **Revert**. Editing a built-in saves a **copy** into - a *My edits* playlist (built-ins never change). Editing your own updates it in place. +- **Edit on the device:** **tap a beat** to cycle it (off → normal → accent → ghost); **tap the instrument + name** for the **lane editor** (sound · beats · subdivision · swing · mute, plus **+ Lane / Remove**). + The title turns **red** (unsaved); **tap the title** to **Save** or **Revert**. Editing a built-in saves a + **copy** into a *My edits* playlist (built-ins never change). Editing your own updates it in place. - **Continue (auto-advance):** tap **CONT** (top-right of the tab line) — when on, a playlist auto-advances to the next item at the end of each item's `b` segment (turn it on for the Song playlist). diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index e4a72693dd90541c269b2b63da5f407a67b93f2b..601ca44e646be4ceebe8425631525f3ca825ace3 100644 GIT binary patch delta 17378 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 MnjU968Y>9@0m`8#HUIzs diff --git a/pico-cp/app.py b/pico-cp/app.py index acc5904..1274ff2 100644 --- a/pico-cp/app.py +++ b/pico-cp/app.py @@ -18,7 +18,7 @@ import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart -APP_VERSION = "0.0.12" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.1.0" # firmware version (the A/B updater pushes/compares this) try: import rtc # set from the editor's clock SysEx so the log has real timestamps except ImportError: @@ -115,6 +115,8 @@ SOUND_GM = {"kick":36,"kick808":36,"kick909":36, "snare":38,"snare808":38,"snare "tomLow":41,"tom808":45,"tomMid":45,"tomHigh":48, "tambourine":54, "cowbell":56,"cowbell808":56, "woodblock":76,"jamblock":76, "claves":75, "beep":37} GM_DEFAULT = 37 +SOUNDS = ["kick", "snare", "clap", "rim", "hatClosed", "hatOpen", "ride", "crash", # lane-editor sound cycle + "tomLow", "tomMid", "tomHigh", "cowbell", "woodblock", "claves", "tambourine", "beep"] MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost MAXLANES = 5 # lanes shown on the pad grid (extras still play) GRID_TOP = 158 # top of the pad grid (leaves room for time/bar/ramp/tab rows) @@ -598,11 +600,9 @@ class App: self.dirty = True def _tap_overlay(self, tx, ty): if self._overlay == 'msg': self._close_overlay(); return - for x0, y0, x1, y1, act in self._ovbtns: - if x0 <= tx <= x1 and y0 <= ty <= y1: - while len(self.g_overlay): self.g_overlay.pop() # clear the panel, then run the action - self._overlay = None; act(); self.dirty = True; return - self._close_overlay() # tapped outside -> cancel + for x0, y0, x1, y1, act in self._ovbtns: # each action manages the panel (lane edits redraw it live) + if x0 <= tx <= x1 and y0 <= ty <= y1: act(); return + self._close_overlay() # tapped outside a button -> cancel / done def _handle_tap(self, tx, ty): if self._overlay: self._tap_overlay(tx, ty); return if 112 <= ty <= 126: # set-list tab line @@ -614,7 +614,79 @@ class App: return hit = self._grid_hit(tx, ty) if hit and hit[0] == 'beat': self._cycle_beat(hit[1], hit[2]); return + if hit and hit[0] == 'lane': self._show_laneedit(hit[1]); return # tap the instrument name -> lane editor self._tap_log(tx, ty) # else the practice log + # ---------- lane editor (tap the instrument name): sound / beats / sub / swing / mute + add / remove ---------- + def _show_laneedit(self, li): + self._overlay = 'lane'; self._edit_li = li; self._draw_laneedit() + def _draw_laneedit(self): + li = self._edit_li; L = self.lanes[li]; g = self.g_overlay + while len(g): g.pop() + self._ovbtns = [] + PX, PY, PW, RH = 14, 54, WIDTH - 28, 34 + g.append(rect(PX, PY, PW, RH * 7 + 30, C_PANEL)); g.append(rect(PX, PY, PW, 2, C_CYAN)) + t, w, h = make_text("Edit lane %d of %d" % (li + 1, len(self.lanes)), FONT_S, C_MUTE, C_PANEL) + t.x = PX + 12; t.y = PY + 8; g.append(t) + y = [PY + 28] + def vrow(label, value, fn): # label + [<] value [>]; left tap = fn(-1), right = fn(+1) + yy = y[0] + lt, lw, lh = make_text(label, FONT_S, C_MUTE, C_PANEL); lt.x = PX + 12; lt.y = yy + 9; g.append(lt) + g.append(rect(PX + 108, yy + 3, 28, RH - 8, C_BTN)) + at, aw, ah = make_text("<", FONT_M, C_CYAN, C_BTN); at.x = PX + 108 + 9; at.y = yy + 7; g.append(at) + vt, vw, vh = make_text(value, FONT_M, C_TXT, C_PANEL); vt.x = PX + 146; vt.y = yy + 5; g.append(vt) + g.append(rect(PX + PW - 36, yy + 3, 28, RH - 8, C_BTN)) + gt, gw, gh = make_text(">", FONT_M, C_CYAN, C_BTN); gt.x = PX + PW - 36 + 9; gt.y = yy + 7; g.append(gt) + self._ovbtns.append((PX + 104, yy, PX + 140, yy + RH, lambda: fn(-1))) + self._ovbtns.append((PX + PW - 40, yy, PX + PW, yy + RH, lambda: fn(1))) + y[0] += RH + vrow("Sound", L['sound'][:9], self._edit_sound) + vrow("Beats", str(sum(L['groups'])), self._edit_beats) + vrow("Subdiv", str(L['sub']), self._edit_sub) + vrow("Swing", "on" if L['swing'] else "off", self._edit_swing) + vrow("Mute", "yes" if L['mute'] else "no", self._edit_mute) + yy = y[0] + 2; bw = (PW - 36) // 2 # + Lane | Remove + g.append(rect(PX + 12, yy, bw, RH - 6, C_BTN)) + a, aw, ah = make_text("+ Lane", FONT_S, C_GREEN if len(self.lanes) < MAXLANES else C_DIM, C_BTN); a.x = PX + 22; a.y = yy + 8; g.append(a) + self._ovbtns.append((PX + 12, yy, PX + 12 + bw, yy + RH, self._edit_add)) + g.append(rect(PX + PW - 12 - bw, yy, bw, RH - 6, C_BTN)) + r, rw, rh = make_text("Remove", FONT_S, C_AMBER if len(self.lanes) > 1 else C_DIM, C_BTN); r.x = PX + PW - 12 - bw + 14; r.y = yy + 8; g.append(r) + self._ovbtns.append((PX + PW - 12 - bw, yy, PX + PW - 12, yy + RH, self._edit_remove)) + yy += RH + 2 + g.append(rect(PX + 12, yy, PW - 24, RH - 4, C_BTN)) + d, dw, dh = make_text("Done", FONT_M, C_CYAN, C_BTN); d.x = PX + (PW - dw) // 2; d.y = yy + 5; g.append(d) + self._ovbtns.append((PX + 12, yy, PX + PW - 12, yy + RH, self._edit_done)) + self.dirty = True + def _regen_levels(self, L): # default accents after a beats/sub change + sub = L['sub']; groups = L['groups']; starts = set(); acc = 0 + for gp in groups: starts.add(acc); acc += gp + L['steps'] = sum(groups) * sub + L['levels'] = [(2 if (i // sub) in starts else 1) if i % sub == 0 else 0 for i in range(L['steps'])] + def _lane_dirty(self, structural): + if structural: self._regen_levels(self.lanes[self._edit_li]) + self.build_grid() + if not self._dirty: self._dirty = True; self.draw_status() + self._draw_laneedit() # refresh the modal with the new values + def _edit_sound(self, d): + L = self.lanes[self._edit_li]; i = SOUNDS.index(L['sound']) if L['sound'] in SOUNDS else 0 + L['sound'] = SOUNDS[(i + d) % len(SOUNDS)]; self._lane_dirty(False) + def _edit_beats(self, d): + L = self.lanes[self._edit_li]; L['groups'] = [max(1, min(12, sum(L['groups']) + d))]; self._lane_dirty(True) + def _edit_sub(self, d): + L = self.lanes[self._edit_li]; L['sub'] = max(1, min(8, L['sub'] + d)); self._lane_dirty(True) + def _edit_swing(self, d): + L = self.lanes[self._edit_li]; L['swing'] = not L['swing']; self._lane_dirty(False) + def _edit_mute(self, d): + L = self.lanes[self._edit_li]; L['mute'] = not L['mute']; self._lane_dirty(False) + def _edit_add(self): + if len(self.lanes) >= MAXLANES: return + self.lanes.insert(self._edit_li + 1, _parse_lane("beep:4")); self._edit_li += 1; self._lane_dirty(False) + def _edit_remove(self): + if len(self.lanes) <= 1: return + del self.lanes[self._edit_li] + if self._edit_li >= len(self.lanes): self._edit_li = len(self.lanes) - 1 + self._lane_dirty(False) + def _edit_done(self): + self._close_overlay() def _step_dur(self, L, step): beat = 60_000_000_000 / self.bpm if L['poly']: # ~ polymeter: fit this lane's whole cycle into lane 1's bar