From dec6c61fce0e8f92b1bd0afc5d7adb4e6b2d1346 Mon Sep 17 00:00:00 2001 From: Me Here Date: Fri, 29 May 2026 08:56:45 -0500 Subject: [PATCH] PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device screen redesign (CircuitPython app.py), built proportional to WIDTH/HEIGHT so it scales to other panels (one adaptive firmware, per-panel config — not a fork): - gen_assets.py bakes logo.bin (VARASYS wordmark, no tagline), midi.bin (DIN-5), usb.bin (trident) as 4-bit-alpha bitmaps (same packing as the fonts). - Header: VARASYS logo (brand cyan) replaces the "PM_K-1 KIT" text; MIDI icon goes green when a host is listening, USB icon lights when supervisor.runtime.usb_connected. load_alpha/make_glyph are non-fatal — a missing .bin falls back to text, never a black screen (addresses the corrupt-file failure mode we just hit). - Pad grid: filled squares on main beats, hollow outline squares (outer+inner rect) on off-beats; playhead fills the lit pad. Vertical gridlines at the master lane's beats (full height) so beats line up across lanes. - Stopwatch (m:ss) + bar counter (master-lane cycles), refreshed ~4x/s only on change. The .bin assets ship in the drive bundle (the A/B updater only pushes app.py), so a one-time re-copy is needed to pick them up. APP_VERSION -> 0.0.2. Co-Authored-By: Claude Opus 4.7 (1M context) --- build.sh | 2 +- pico-cp/README.md | 7 +- pico-cp/__pycache__/app.cpython-312.pyc | Bin 47106 -> 53218 bytes pico-cp/app.py | 126 +++++++++++++++++---- pico-cp/gen_assets.py | 139 ++++++++++++++++++++++++ pico-cp/logo.bin | Bin 0 -> 2264 bytes pico-cp/midi.bin | Bin 0 -> 244 bytes pico-cp/usb.bin | Bin 0 -> 244 bytes 8 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 pico-cp/gen_assets.py create mode 100644 pico-cp/logo.bin create mode 100644 pico-cp/midi.bin create mode 100644 pico-cp/usb.bin diff --git a/build.sh b/build.sh index 2a6b795..3b60e88 100755 --- a/build.sh +++ b/build.sh @@ -42,7 +42,7 @@ print("copied pico-cp-app.py") import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY) with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z: for f in ("code.py", "app.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin", - "README.md", "protect-firmware.sh"): + "logo.bin", "midi.bin", "usb.bin", "README.md", "protect-firmware.sh"): z.write("pico-cp/" + f, f) z.write("dist/editor.html", "editor.html") # offline copy of the editor, on the drive print("zipped pm_k1_circuitpy.zip") diff --git a/pico-cp/README.md b/pico-cp/README.md index 3a994a7..0732b83 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -24,8 +24,11 @@ from the web editor over USB‑MIDI**, and plays through your **computer's speak ( — Pico 2 / W builds also fine). A `CIRCUITPY` drive appears. 2. **Copy the whole bundle** onto `CIRCUITPY`: `boot.py`, `code.py` (loader) + `app.py` (the application), - `programs.json`, `font_s.bin` / `font_m.bin` / `font_l.bin`, `editor.html` (offline editor), and the - helper scripts. (`code.py` is a tiny stable loader; `app.py` is what firmware updates replace.) + `programs.json`, `font_s.bin` / `font_m.bin` / `font_l.bin`, `logo.bin` / `midi.bin` / `usb.bin` (the + on-screen logo + MIDI/USB status icons), `editor.html` (offline editor), and the helper scripts. + (`code.py` is a tiny stable loader; `app.py` is what firmware updates replace. The `.bin` assets — like + the fonts — ride in the bundle, since the one-click updater only pushes `app.py`; if a `.bin` is missing + the firmware just falls back to text and never fails to boot.) 3. **Power‑cycle** (so `boot.py` takes effect). It boots into appliance mode and runs. ## Program it from the web (push over USB‑MIDI) diff --git a/pico-cp/__pycache__/app.cpython-312.pyc b/pico-cp/__pycache__/app.cpython-312.pyc index cfd5ae9545e3bf1fa649ab169211fb669099473f..d8a869830d4483242123439a201b86c96a002841 100644 GIT binary patch delta 14070 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 caller falls back to text (no crash) +def make_glyph(asset, fg, bg): + w, h, blob = asset + gc.collect() + bmp = displayio.Bitmap(w, h, 16); pal = displayio.Palette(16) + for i in range(16): pal[i] = _blend(bg, fg, i) + for k in range(w * h): + byte = blob[2 + (k >> 1)] + nib = (byte >> 4) if (k & 1) == 0 else (byte & 0xF) + if nib: bmp[k % w, k // w] = nib + return displayio.TileGrid(bmp, pixel_shader=pal), pal, w, h +def _recolor(pal, fg, bg): # re-tint a stored asset palette in place (tear-free) + for i in range(16): pal[i] = _blend(bg, fg, i) + +LOGO = load_alpha("/logo.bin") # VARASYS wordmark (no tagline) +ICON_MIDI = load_alpha("/midi.bin") # DIN-5: green when a MIDI host is listening +ICON_USB = load_alpha("/usb.bin") # trident: lit when USB-connected to a computer +gc.collect() + # ============================== POLYMETER ENGINE (same semantics as the web/MicroPython) ============================== PAT = {'X': 2, 'x': 1, 'g': 3, '.': 0, '-': 0, '_': 0} PRIO = {2: 3, 1: 2, 3: 1} @@ -312,9 +340,13 @@ class App: self.running = False; self.bpm = 120; self.idx = 0; self.lanes = []; self.rgb = (0, 0, 0) self.programs = load_programs() self.dirty = True - self.pad_pal = displayio.Palette(8) + self.pad_pal = displayio.Palette(10) # 0-3 idle levels, 4-7 lit levels, 8 off-beat border, 9 hollow bg for i in range(4): self.pad_pal[i] = PAD_DIM[i]; self.pad_pal[i + 4] = PAD_LIT[i] + self.pad_pal[8] = PAD_OUTLINE; self.pad_pal[9] = C_BG self.lane_pads = []; self.lane_lit = [] + self.usb_conn = False; self._m_steps = 0 # USB-connected state; master-lane steps (for the bar counter) + self._uiNext = 0.0; self._lastTs = None; self._lastBs = None # throttle the stopwatch/bar redraw + self.ic_midi_pal = None; self.ic_usb_pal = None # practice history - persisted to /history.json (next to programs.json) when we own the filesystem self.can_write = self._probe_write() self.log = self._load_log() @@ -322,7 +354,7 @@ class App: self._armed = None; self.log_rows = [] self._build_scene() self.load(0) - self.draw_log() + self.draw_log(); self.draw_icons(); self.draw_meters() def _btn(self, pin): d = digitalio.DigitalInOut(pin); d.direction = digitalio.Direction.INPUT; d.pull = digitalio.Pull.UP @@ -332,13 +364,23 @@ class App: def _build_scene(self): root = displayio.Group(); self.display.root_group = root root.append(rect(0, 0, WIDTH, HEIGHT, C_BG)) - tg, w, h = make_text("PM_K-1 KIT", FONT_M, C_CYAN, C_BG); tg.x = 12; tg.y = 8; root.append(tg) + # header: VARASYS logo (left, no tagline) + MIDI / USB status icons (right) + if LOGO: + tg, _p, lw, lh = make_glyph(LOGO, C_CYAN, C_BG); tg.x = 10; tg.y = 9; root.append(tg) + else: + tg, w, h = make_text("VARASYS", FONT_M, C_CYAN, C_BG); tg.x = 10; tg.y = 8; root.append(tg) + x = WIDTH - 12 + for asset, attr in ((ICON_USB, "ic_usb_pal"), (ICON_MIDI, "ic_midi_pal")): + if asset: + tg, pal, w, h = make_glyph(asset, C_DIM, C_BG); x -= w; tg.x = x; tg.y = 8; x -= 8 + root.append(tg); setattr(self, attr, pal) root.append(rect(0, 38, WIDTH, 2, C_PANEL)) # dynamic groups 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_time = displayio.Group(); root.append(self.g_time) # stopwatch (m:ss, left) + self.g_bar = displayio.Group(); root.append(self.g_bar) # bar counter (right) self.g_name = displayio.Group(); root.append(self.g_name) # item index + name - self.g_midi = displayio.Group(); root.append(self.g_midi) # "MIDI" indicator (top-right) when a host is listening self.g_grid = displayio.Group(); root.append(self.g_grid) # lanes x step pads root.append(rect(0, LOG_TOP - 6, WIDTH, 2, C_PANEL)) # divider above the history log self.g_log = displayio.Group(); root.append(self.g_log) # practice history (tap a row to delete) @@ -376,6 +418,7 @@ class App: now = time.monotonic_ns() for L in self.lanes: L['next'] = now; L['step'] = -1 + self._m_steps = 0 # restart the bar count # ---------- audio + light ---------- def click(self, level): @@ -428,6 +471,7 @@ class App: adv = False while now >= L['next']: L['step'] = (L['step'] + 1) % L['steps'] + if li == 0: self._m_steps += 1 # count master-lane steps -> bars lvl = 0 if L['mute'] else L['levels'][L['step']] if lvl > 0: fired.append(lvl) @@ -482,7 +526,10 @@ class App: if host != self.midi_host: self.midi_host = host if host: self.buz.duty_cycle = 0 # silence the buzzer when the computer takes over - self.led_off(); self.draw_midi() + self.led_off(); self.draw_icons() + uc = bool(getattr(supervisor.runtime, "usb_connected", True)) # connected to a computer? + if uc != self.usb_conn: + self.usb_conn = uc; self.draw_icons() # ---------- drawing ---------- def draw_bpm(self): @@ -491,42 +538,78 @@ class App: 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_name, "%d/%d %s" % (self.idx+1, len(self.programs), self.name[:18]), - 12, 112, C_TXT, C_BG, FONT_M) - def draw_midi(self): - self._place(self.g_midi, "MIDI" if self.midi_host else "", 0, 12, C_GREEN, C_BG, FONT_M, right_edge=WIDTH-12) + 12, 120, C_TXT, C_BG, FONT_M) + def draw_icons(self): # recolor the MIDI/USB icons by state (tear-free palette swap) + if self.ic_midi_pal is not None: + _recolor(self.ic_midi_pal, C_GREEN if self.midi_host else C_DIM, C_BG) + if self.ic_usb_pal is not None: + _recolor(self.ic_usb_pal, C_CYAN if self.usb_conn else C_DIM, C_BG) + self.dirty = True + def draw_meters(self): # stopwatch (m:ss) + bar counter; called ~4x/s from run() + if self.running and self.play_start is not None: + el = int(time.monotonic() - self.play_start) + ts = "%d:%02d" % (el // 60, el % 60) + mlen = self.lanes[0]['steps'] if self.lanes else 1 + bs = "bar %d" % (self._m_steps // max(1, mlen) + 1) + else: + ts = "0:00"; bs = "bar -" + if ts != self._lastTs: + self._place(self.g_time, ts, 12, 86, C_TXT, C_BG, FONT_M); self._lastTs = ts + if bs != self._lastBs: + self._place(self.g_bar, bs, 0, 92, C_MUTE, C_BG, FONT_M, right_edge=WIDTH-12); self._lastBs = bs # ---------- 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 _sq(self, cx, cy, side, ci): # a centred square pad sharing pad_pal + r = vectorio.Rectangle(pixel_shader=self.pad_pal, width=side, height=side, x=cx - side // 2, y=cy - side // 2) + r.color_index = ci; return r 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(40, (296 - top) // max(1, n)) + top = GRID_TOP; rowh = min(40, ((LOG_TOP - 10) - top) // max(1, n)) + px0 = 60; usable = WIDTH - 8 - px0 - 12; gridh = n * rowh + # vertical gridlines at the master lane's beats, full height -> beats line up across lanes + m = self.lanes[0]; mbeats = max(1, m['steps'] // max(1, m['sub'])) + for bcol in range(mbeats): + self.g_grid.append(rect(px0 + 6 + (bcol * usable) // mbeats, top, 1, gridh, C_GRID)) for li in range(n): L = self.lanes[li]; y = top + li * rowh; cy = y + rowh // 2 tg, w, h = make_text((L.get('sound', '') or '?')[:7], FONT_S, C_MUTE, C_BG) tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg) - steps = L['steps']; sub = L['sub']; px0 = 60 - usable = WIDTH - 8 - px0 - 12; stepw = max(1, usable // steps) - r_big = max(2, min(6, stepw // 2, (rowh - 8) // 2)); r_sml = max(2, r_big - 2) + steps = L['steps']; sub = L['sub']; stepw = max(1, usable // steps) + side = max(5, min(15, stepw - 1, rowh - 6)); inner = max(2, side - 4) pads = [] for s in range(steps): - rad = r_big if (s % sub == 0) else r_sml # big = beat (division), small = subdivision + base = self._padbase(L, s) cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes - c = vectorio.Circle(pixel_shader=self.pad_pal, radius=rad, x=cxp, y=cy) - c.color_index = self._padbase(L, s); self.g_grid.append(c); pads.append(c) + if s % sub == 0: # main beat -> filled square + sq = self._sq(cxp, cy, side, base); self.g_grid.append(sq) + pads.append(("fill", (sq,), base)) + else: # off-beat -> hollow outline square + out = self._sq(cxp, cy, side, base if base else 8) + ins = self._sq(cxp, cy, inner, 9) # bg fill = the hollow centre + self.g_grid.append(out); self.g_grid.append(ins) + pads.append(("out", (out, ins), base)) self.lane_pads.append(pads); self.lane_lit.append(-1) self.dirty = True + def _pad_idle(self, pad): + kind, shapes, base = pad + if kind == "fill": shapes[0].color_index = base + else: shapes[0].color_index = base if base else 8; shapes[1].color_index = 9 # ring + hollow centre + def _pad_lit(self, pad): + kind, shapes, base = pad + for sh in shapes: sh.color_index = base + 4 # fill the square (lit level) regardless of shape 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 + if 0 <= prev < len(pads): self._pad_idle(pads[prev]) + if step < len(pads): self._pad_lit(pads[step]) 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) + if 0 <= prev < len(pads): self._pad_idle(pads[prev]) self.lane_lit[li] = -1 self.dirty = True @@ -635,7 +718,10 @@ class App: except OSError: committed = True while True: self.tick(); self.poll() - if not committed and time.monotonic() - boot > 5: # booted & ran fine for 5s -> confirm the update + tnow = time.monotonic() + if tnow >= self._uiNext: # ~4x/s: tick the stopwatch + bar counter + self._uiNext = tnow + 0.25; self.draw_meters() + if not committed and tnow - boot > 5: # booted & ran fine for 5s -> confirm the update try: os.remove("/trial") except Exception: pass committed = True diff --git a/pico-cp/gen_assets.py b/pico-cp/gen_assets.py new file mode 100644 index 0000000..869a5c1 --- /dev/null +++ b/pico-cp/gen_assets.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# Generate the on-screen bitmap assets the CircuitPython firmware (app.py) blits: +# logo.bin - the VARASYS wordmark (no tagline), tinted brand cyan at the top +# midi.bin - a 5-pin DIN icon (lights green when a MIDI host is listening) +# usb.bin - the USB "trident" icon (lights when the device is USB-connected) +# +# Each file is a single 4-bit-alpha image with a 2-byte header (matches the font packing +# in gen_font.py, just without the glyph metrics table): +# byte 0 = width, byte 1 = height, then ((w*h+1)//2) bytes of 4-bit alpha, +# row-major, two pixels per byte (first pixel = high nibble). +# app.py's load_alpha()/make_glyph() decode it and blend bg->fg per pixel (smooth). +# +# Re-run after changing the logo/icons: python3 pico-cp/gen_assets.py +# Writes pico-cp/{logo,midi,usb}.bin and /tmp/assets_verify.png (eyeball it). + +import math, pathlib +from PIL import Image, ImageDraw + +HERE = pathlib.Path(__file__).parent +LOGO_PNG = pathlib.Path.home() / "src/varasys_logo/For using with dark colored background/VARASYS Limited.png" +SS = 6 # supersample factor for the drawn icons (downscaled -> anti-aliased) + + +def pack(coverage_img): + """coverage_img: 'L' image (0..255 alpha). -> bytes(w, h, packed 4-bit alpha).""" + w, h = coverage_img.size + assert w <= 255 and h <= 255, "asset too large for the 1-byte dims (%dx%d)" % (w, h) + px = coverage_img.load() + nib = [] + for y in range(h): + for x in range(w): + nib.append(px[x, y] >> 4) # 8-bit -> 4-bit alpha + if len(nib) % 2: + nib.append(0) + out = bytearray([w, h]) + for i in range(0, len(nib), 2): + out.append((nib[i] << 4) | nib[i + 1]) + return bytes(out) + + +def make_logo(target_w=156): + img = Image.open(LOGO_PNG).convert("RGBA") + r, g, b, a = img.split() + # Coverage = alpha if the PNG is genuinely transparent, else brightness (cyan on a flat bg). + if a.getextrema()[0] < 250: + cov = a + else: + cov = img.convert("L") + bbox = cov.getbbox() + if bbox: + cov = cov.crop(bbox) + w, h = cov.size + th = max(1, round(target_w * h / w)) + cov = cov.resize((target_w, th), Image.LANCZOS) + return pack(cov) + + +def make_midi(size=22): + cw = size * SS + img = Image.new("L", (cw, cw), 0) + d = ImageDraw.Draw(img) + cx = cy = cw / 2 + R = cw * 0.44 + lw = max(2, int(cw * 0.07)) + d.ellipse([cx - R, cy - R, cx + R, cy + R], outline=255, width=lw) # connector shell + # 5 pins in the DIN fan (one top-centre, a flanking pair above, a wider pair below) + ring = R * 0.60 + pr = cw * 0.075 + pins = [(0.0, -1.0), (-0.72, -0.55), (0.72, -0.55), (-0.95, 0.18), (0.95, 0.18)] + for fx, fy in pins: + x = cx + fx * ring + y = cy + fy * ring + d.ellipse([x - pr, y - pr, x + pr, y + pr], fill=255) + return pack(img.resize((size, size), Image.LANCZOS)) + + +def make_usb(size=22): + cw = size * SS + img = Image.new("L", (cw, cw), 0) + d = ImageDraw.Draw(img) + cx = cw / 2 + lw = max(2, int(cw * 0.075)) + top, bot = cw * 0.10, cw * 0.90 + d.line([(cx, top + cw * 0.10), (cx, bot)], fill=255, width=lw) # shaft + # arrowhead at the top + ah = cw * 0.13 + d.polygon([(cx, top), (cx - ah, top + ah * 1.4), (cx + ah, top + ah * 1.4)], fill=255) + # base plug (filled circle at the bottom) + br = cw * 0.10 + d.ellipse([cx - br, bot - br, cx + br, bot + br], fill=255) + # left branch -> small filled circle + ly = cw * 0.46 + lx = cx - cw * 0.26 + d.line([(cx, ly + cw * 0.10), (lx, ly)], fill=255, width=lw) + cr = cw * 0.085 + d.ellipse([lx - cr, ly - cr, lx + cr, ly + cr], fill=255) + # right branch -> small filled square + ry = cw * 0.34 + rx = cx + cw * 0.26 + d.line([(cx, ry + cw * 0.10), (rx, ry)], fill=255, width=lw) + sq = cw * 0.08 + d.rectangle([rx - sq, ry - sq, rx + sq, ry + sq], fill=255) + return pack(img.resize((size, size), Image.LANCZOS)) + + +def unpack_to_img(blob, fg=(10, 179, 247), bg=(6, 9, 14)): + """Decode like app.py would, for the verify sheet.""" + w, h = blob[0], blob[1] + im = Image.new("RGB", (w, h), bg) + for k in range(w * h): + byte = blob[2 + (k >> 1)] + nib = (byte >> 4) if (k & 1) == 0 else (byte & 0xF) + t = nib * 17 + col = tuple((bg[i] * (255 - t) + fg[i] * t) // 255 for i in range(3)) + im.putpixel((k % w, k // w), col) + return im + + +def main(): + assets = {"logo": make_logo(), "midi": make_midi(), "usb": make_usb()} + for name, blob in assets.items(): + (HERE / (name + ".bin")).write_bytes(blob) + print("wrote %s.bin %dx%d %d bytes" % (name, blob[0], blob[1], len(blob))) + # verify sheet on a dark panel-like background + pad = 12 + imgs = [unpack_to_img(b) for b in assets.values()] + W = max(i.width for i in imgs) + pad * 2 + H = sum(i.height for i in imgs) + pad * (len(imgs) + 1) + sheet = Image.new("RGB", (W, H), (6, 9, 14)) + y = pad + for i in imgs: + sheet.paste(i, (pad, y)) + y += i.height + pad + sheet.save("/tmp/assets_verify.png") + print("verify -> /tmp/assets_verify.png") + + +if __name__ == "__main__": + main() diff --git a/pico-cp/logo.bin b/pico-cp/logo.bin new file mode 100644 index 0000000000000000000000000000000000000000..21eb897693927ad1fc606d63a9187538a6f48935 GIT binary patch literal 2264 zcmd^9y=zlZ6hG;=t(7;ZlP%dq1oQp@Y1{;b2BCAacQ7bAh>(tTvfv_AOhFW_)Minn zsV3l1u+e02Q+T#Rp|ld(e6;y^@Ay0C-WS~M)-&9DKJNXU&wHPZJzOgu8*Qv-_!y~q zlF)*+l9bNRd9ty;RMDM(=5o0;%QJsnF9&Z>DFU7Ml!r8InIA-v8{iLV9BYR}--#2QD=8W$`sLtakF21$oB}BtT`&S~_8P`_ z#7P>0E7z*(yCAwDgO-g{ppQdRTC~i^xw*M}6#8foe1mFqDR+l5;0lyyp4Yo>@7q@n zf|Ln45Tj3LZ~{Sl2O)rnS)2c7pQc6T-w}_4`@$VkjD5kt-7C!7X@l0J39(H>58vq{ z3!q{{lVZNi@ljP2Y{n3xL)_F`ZJJb@#uhlICBVFD%CW$)Hj$4^Pg(<<9W-`(4nmeZ zcBM$2u)2~dCkJ4%K2sx{1b9d_dMs=$QmL8(98mu|YiuB}RplL;5mb$h3n*p6)IM;= zxd8BxX5k-H=tEK&;o9ilbx$}BqB>gO7KzS2$F8weX(iCGg)X_4D)9rS&x$)@P;`E7wx<1 zLeT**T>526n##X1E2d|^3mC2N&wh6PQ()x-touo`l-He(RU|Tz#7xZpyK|+-1j73XH zVL+~FmcAlItI9(x3#4>5kM%viq?;lF&50SNeT}HE)J5CXAC$T1soDRAZSOmlpol|z jB7%wvy8C9?i1>It%LS_o!rIuZ@_)Azgf_EujP&6zZu2>4 literal 0 HcmV?d00001 diff --git a/pico-cp/midi.bin b/pico-cp/midi.bin new file mode 100644 index 0000000000000000000000000000000000000000..02f1cc548f1626d63fcd0f75833f0f7f3354c47f GIT binary patch literal 244 zcmWd=V?Y5+-FJ5=fJK=0|6O(eH<%s&O(2vp|0__K^|t`~{{qJS0SpZK8yL9%1Mw#Y zhVv|MgzhSybNIu+^o{p_0|UqZ3;7BhAG!VqFtGk#Am6}qfgu&BIgEk#0Plt@10a)m z8TSRgMgM_%8T9{aJ>WS4W%?Z8_)wDo6lFWk-@x<@$N+(S1%~qiU>4&au*D!Y$YP*# dfGn_c7=VssfH;x?==9xCr!yct1U3Lf005KPLX`jj literal 0 HcmV?d00001 diff --git a/pico-cp/usb.bin b/pico-cp/usb.bin new file mode 100644 index 0000000000000000000000000000000000000000..dd86644637f6b4ff3b256baad08e4a3188add60c GIT binary patch literal 244 zcmWd=W55ku5mN63U~I1c8(_@(|35@S6~MgEb0PZw8z2&U8T