diff --git a/pico-cp/__pycache__/code.cpython-312.pyc b/pico-cp/__pycache__/code.cpython-312.pyc index dfa6936..905e9d1 100644 Binary files a/pico-cp/__pycache__/code.cpython-312.pyc and b/pico-cp/__pycache__/code.cpython-312.pyc differ diff --git a/pico-cp/code.py b/pico-cp/code.py index 898f7c0..e56e710 100644 --- a/pico-cp/code.py +++ b/pico-cp/code.py @@ -62,6 +62,9 @@ C_BG, C_PANEL, C_TXT, C_MUTE = 0x06090E, 0x1C222C, 0xC7D0DB, 0x788494 C_CYAN, C_AMBER, C_GREEN, C_DIM = 0x0AB3F7, 0xFF9B2E, 0x2FE07A, 0x243240 C_BTN = 0x1C222C LEVEL_RGB = {2: (255, 110, 0), 1: (0, 150, 255), 3: (130, 70, 255)} +MAXLANES = 5 # lanes shown on the pad grid (extras still play) +PAD_DIM = (0x10161E, 0x0A3A52, 0x4A3010, 0x2A1D4A) # idle pad: mute / normal / accent / ghost +PAD_LIT = (0x39414D, 0x0AB3F7, 0xFF9B2E, 0x967BFF) # playhead pad: mute / normal / accent / ghost # WS2812 RGB LED — self-contained via the core neopixel_write module (no external library) class RGB: @@ -174,7 +177,7 @@ def _parse_lane(tok): for i in range(steps): if i % sub == 0: levels.append(2 if (i // sub) in starts else 1) else: levels.append(0) - return {'sub': sub, 'steps': steps, 'levels': levels, 'poly': poly, 'mute': mute} + return {'sound': sound, 'sub': sub, 'steps': steps, 'levels': levels, 'poly': poly, 'mute': mute} def load_programs(): try: @@ -278,9 +281,10 @@ class App: self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.rgb = (0, 0, 0) self.programs = load_programs() self.buttons = [] - self.dirty = True; self.dots = [] - self.dot_pal = displayio.Palette(3) - self.dot_pal[0] = C_DIM; self.dot_pal[1] = C_CYAN; self.dot_pal[2] = C_AMBER + self.dirty = True + self.pad_pal = displayio.Palette(8) + for i in range(4): self.pad_pal[i] = PAD_DIM[i]; self.pad_pal[i + 4] = PAD_LIT[i] + self.lane_pads = []; self.lane_lit = [] self._build_scene() self.load(0) @@ -292,16 +296,13 @@ class App: def _build_scene(self): root = displayio.Group(); self.display.root_group = root root.append(rect(0, 0, WIDTH, HEIGHT, C_BG)) - root.append(rect(0, 42, WIDTH, 2, C_PANEL)) - # static title + BPM label - tg, w, h = make_text("PM_K-1 KIT", FONT_M, C_CYAN, C_BG); tg.x = 12; tg.y = 12; root.append(tg) - tg, w, h = make_text("BPM", FONT_M, C_MUTE, C_BG); tg.x = 12; tg.y = 120; root.append(tg) + tg, w, h = make_text("PM_K-1 KIT", FONT_M, C_CYAN, C_BG); tg.x = 12; tg.y = 8; root.append(tg) + root.append(rect(0, 38, WIDTH, 2, C_PANEL)) # dynamic groups - self.g_bpm = displayio.Group(); root.append(self.g_bpm) - self.g_dots = displayio.Group(); root.append(self.g_dots) - self.g_run = displayio.Group(); root.append(self.g_run) - self.g_idx = displayio.Group(); root.append(self.g_idx) - self.g_name = displayio.Group(); root.append(self.g_name) + self.g_bpm = displayio.Group(); root.append(self.g_bpm) # big tempo (right) + self.g_run = displayio.Group(); root.append(self.g_run) # RUN / STOP (left) + self.g_name = displayio.Group(); root.append(self.g_name) # item index + name + self.g_grid = displayio.Group(); root.append(self.g_grid) # lanes × step pads # buttons (rects static; labels in per-button groups so play can toggle) bw, bh = 96, 56; gap = (WIDTH - 3*bw)//4; xs = [gap, gap*2+bw, gap*3+bw*2] self.btn_lbl = {} @@ -335,8 +336,8 @@ class App: n = len(self.programs); self.idx = i % n self.name, prog = self.programs[self.idx] self.bpm, self.lanes = parse_program(prog) - self.master = self.lanes[0]; self.beat = -1 - self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_dots() + self.master = self.lanes[0] + self._reset_clock(); self.draw_bpm(); self.draw_status(); self.build_grid() def _reset_clock(self): now = time.monotonic_ns() for L in self.lanes: @@ -357,9 +358,9 @@ class App: # ---------- transport ---------- def toggle(self): self.running = not self.running - if self.running: self._reset_clock(); self.beat = -1 - else: self.buz.duty_cycle = 0; self.led_off() - self.draw_status(); self.draw_dots(); self._label("play") + if self.running: self._reset_clock() + else: self.buz.duty_cycle = 0; self.led_off(); self.reset_playheads() + self.draw_status(); self._label("play") def set_bpm(self, v): v = max(30, min(300, v)) if v != self.bpm: @@ -368,7 +369,7 @@ class App: self.draw_bpm() def goto(self, i): was = self.running; self.load(i); self._label("play") - if was: self.running = True; self._reset_clock(); self.beat = -1 + if was: self.running = True; self._reset_clock() def tap(self): now = time.monotonic() if not hasattr(self, '_taps'): self._taps = [] @@ -383,18 +384,17 @@ class App: now = time.monotonic_ns() if self.buz_off and now >= 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);