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 e4a7269..601ca44 100644 Binary files a/pico-cp/__pycache__/app.cpython-312.pyc and b/pico-cp/__pycache__/app.cpython-312.pyc differ 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