From c499910df4f7841e1de707f66f57ae9cdc118231 Mon Sep 17 00:00:00 2001 From: Me Here Date: Thu, 28 May 2026 21:56:27 -0500 Subject: [PATCH] PM_K-1 CircuitPython: add the lanes/pads view Replace the single beat-dot row with a full pad grid: each lane is a row of step pads coloured by dynamics (mute/normal/accent/ghost), with the playhead lit as it plays (per-lane, so polymeter shows). Header (title/BPM/RUN/item) is compacted above it; transport stays below. Pads are vectorio rects sharing one 8-colour palette and recolour in place via color_index (cheap, tear-free); the grid only rebuilds on track change. Caps at MAXLANES=5 rows (extra lanes still play). Verified by rendering the whole displayio scene graph headless (layout + playhead lighting correct). Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/__pycache__/code.cpython-312.pyc | Bin 33689 -> 35070 bytes pico-cp/code.py | 101 ++++++++++++++--------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/pico-cp/__pycache__/code.cpython-312.pyc b/pico-cp/__pycache__/code.cpython-312.pyc index dfa69362d7e62a95b4c428072e3dcc4b5da8bdb3..905e9d18f76728723f471ee743fbcf666510e452 100644 GIT binary patch delta 10787 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 delta 9407 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 self.buz_off: self.buz.duty_cycle = 0; self.buz_off = 0 if self.running: - fired = []; beat_hit = False - for L in self.lanes: + fired = [] + for li, L in enumerate(self.lanes): + adv = False while now >= L['next']: L['step'] = (L['step'] + 1) % L['steps'] lvl = 0 if L['mute'] else L['levels'][L['step']] if lvl > 0: fired.append(lvl) - if L is self.master and L['step'] % L['sub'] == 0: beat_hit = True - L['next'] += L['dur'] + L['next'] += L['dur']; adv = True + if adv and li < len(self.lane_pads): self._move_playhead(li, L['step']) if fired: best = max(fired, key=lambda l: PRIO.get(l, 0)); self.click(best); self.flash(best) - if beat_hit: - self.beat = self.master['step'] // self.master['sub']; self.draw_dots() if self.rgb != (0, 0, 0): r, g, b = self.rgb; r = r*7//10; g = g*7//10; b = b*7//10 self.rgb = (r, g, b) if (r+g+b) > 12 else (0, 0, 0) @@ -442,28 +442,47 @@ class App: # ---------- drawing ---------- def draw_bpm(self): - self._place(self.g_bpm, str(self.bpm), 0, 92, C_TXT, C_BG, FONT_L, right_edge=WIDTH-14) + self._place(self.g_bpm, str(self.bpm), 0, 44, C_TXT, C_BG, FONT_L, right_edge=WIDTH-12) def draw_status(self): - self._place(self.g_run, "RUN" if self.running else "STOP", 12, 244, + self._place(self.g_run, "RUN" if self.running else "STOP", 12, 48, C_GREEN if self.running else C_MUTE, C_BG, FONT_M) - self._place(self.g_idx, "%d/%d" % (self.idx+1, len(self.programs)), 0, 244, C_MUTE, C_BG, FONT_M, right_edge=WIDTH-12) - self._place(self.g_name, self.name[:26], 12, 272, C_TXT, C_BG, FONT_M) - def draw_dots(self): - m = self.master; bpb = max(1, m['steps'] // m['sub']) - if len(self.dots) != bpb: # rebuild only when the beat count changes - while len(self.g_dots): self.g_dots.pop() - self.dots = []; sz, sp = 18, 26; x0 = max(12, WIDTH - 12 - bpb*sp) - for i in range(bpb): - r = vectorio.Rectangle(pixel_shader=self.dot_pal, width=sz, height=sz, x=x0 + i*sp, y=200) - self.g_dots.append(r); self.dots.append(r) - for i in range(bpb): # otherwise just recolour (cheap, no tearing) - lvl = m['levels'][(i*m['sub']) % m['steps']]; on = self.running and i == self.beat - self.dots[i].color_index = (2 if lvl == 2 else 1) if on else 0 + self._place(self.g_name, "%d/%d %s" % (self.idx+1, len(self.programs), self.name[:18]), + 12, 112, C_TXT, C_BG, FONT_M) + + # ---------- pad grid (each lane = a row of step pads; playhead lit as it plays) ---------- + def _padbase(self, L, s): + return 0 if L['mute'] else L['levels'][s] + def build_grid(self): + while len(self.g_grid): self.g_grid.pop() + self.lane_pads = []; self.lane_lit = [] + n = min(len(self.lanes), MAXLANES) + top = 140; rowh = min(38, (294 - top) // max(1, n)) + for li in range(n): + L = self.lanes[li]; y = top + li * rowh + tg, w, h = make_text((L.get('sound', '') or '?')[:4], FONT_M, C_MUTE, C_BG) + tg.x = 10; tg.y = y + 2; self.g_grid.append(tg) + steps = L['steps']; px0 = 66; pw = (WIDTH - 10 - px0) // steps; ph = max(8, rowh - 10) + pads = [] + for s in range(steps): + r = vectorio.Rectangle(pixel_shader=self.pad_pal, width=max(2, pw - 1), height=ph, x=px0 + s*pw, y=y) + r.color_index = self._padbase(L, s); self.g_grid.append(r); pads.append(r) + self.lane_pads.append(pads); self.lane_lit.append(-1) + self.dirty = True + def _move_playhead(self, li, step): + pads = self.lane_pads[li]; prev = self.lane_lit[li] + if 0 <= prev < len(pads): pads[prev].color_index = self._padbase(self.lanes[li], prev) + if step < len(pads): pads[step].color_index = self._padbase(self.lanes[li], step) + 4 + self.lane_lit[li] = step; self.dirty = True + def reset_playheads(self): + for li, pads in enumerate(self.lane_pads): + prev = self.lane_lit[li] + if 0 <= prev < len(pads): pads[prev].color_index = self._padbase(self.lanes[li], prev) + self.lane_lit[li] = -1 self.dirty = True def run(self): if self.touch.addr is None: - self._place(self.g_name, "touch: not found", 12, 272, C_AMBER, C_BG, FONT_M) + print("GT911 touch not found") while True: self.tick(); self.poll() # push a complete frame only when something changed (no mid-update tearing);