From 7ccc75e399791b19c0c52b5399576f5e79958e9a Mon Sep 17 00:00:00 2001 From: Me Here Date: Thu, 28 May 2026 22:40:08 -0500 Subject: [PATCH] =?UTF-8?q?Phase=203:=20USB-MIDI=20audio=20=E2=80=94=20pla?= =?UTF-8?q?y=20the=20device=20through=20the=20computer's=20speakers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware (pico-cp/code.py): on every click, send a USB-MIDI note-on per firing lane — GM drum note by voice (SOUND_GM), velocity by level (accent/normal/ghost) — via the default-enabled usb_midi.ports[1]. Polyphonic, so the computer plays the full groove. New CONFIG: MIDI_ENABLED (default on), MUTE_BUZZER (silence the buzzer when using computer audio). Editor (editor.html): a '🎹 Device audio' toggle uses the Web MIDI API (requestMIDIAccess) to voice incoming notes through the existing synth — Note-On -> GM_NUM[note] / velocity-to-gain -> playInstrument(). The device is the clock; the browser is the sound module, locked in sync. Chrome/Edge. Verified: firmware emits the right notes (kick+hat on beat 1 of four-on-the-floor, snare's rest skipped); editor loads clean with the toggle + handlers present. Docs (info-kit, both READMEs) updated. The on-device buzzer/screen still work standalone. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 10 +++++-- editor.html | 26 +++++++++++++++++- info-kit.html | 7 +++-- pico-cp/README.md | 9 +++++++ pico-cp/__pycache__/code.cpython-312.pyc | Bin 35600 -> 37303 bytes pico-cp/code.py | 32 ++++++++++++++++++++--- 6 files changed, 75 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 17950d1..252e714 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,12 @@ flashing steps. Firmware lives in **`pico/`**: WS2812 RGB, PWM buzzer, ADC joystick, baked anti‑aliased fonts, and the polymeter engine. It parses the same program strings as the web editor. Flash MicroPython, copy `main.py`, edit the `PROGRAMS` list to change grooves. Download: `/pico-main.py`. -- **`pico/gen_font.py`** — generates the baked anti‑aliased fonts embedded in the firmware. +- **`pico/gen_font.py`** — generates the baked anti‑aliased fonts (used by both firmwares). +- **`pico-cp/`** — a **CircuitPython** edition (download `/pm_k1_circuitpy.zip`): the Pico mounts as a + USB drive carrying the firmware + your `programs.json` + a copy of the editor, with a full lanes/pads + touchscreen display. Design grooves on the web and **Save to device** straight onto the drive (the + editor's ⋯ menu), and play it **out your computer's speakers over USB‑MIDI** (the editor's + **🎹 Device audio** button). The MicroPython build stays the simple, no‑computer option. ## Keyboard shortcuts @@ -261,7 +266,8 @@ tags the current commit `v` (requires a clean tree). Push the tag, then | `embed.html` · `embed.js` | embed docs and the drop‑in widget loader | | `src/` | shared partials inlined into every page: `engine.js`, `setlists.js`, `base.css`, `header.html`, `footer.html`, `chrome.js`, `progbox.{html,js}`, `infoembed.{html,js}` | | `assets/` | base64 blobs inlined at build (`favicon`, `logo-dark`, `logo-light`) | -| `pico/` | PM_K‑1 firmware: `main.py` (MicroPython), `gen_font.py` (font generator), `README.md` | +| `pico/` | PM_K‑1 MicroPython firmware: `main.py`, `gen_font.py` (font generator), `README.md` | +| `pico-cp/` | PM_K‑1 CircuitPython edition: `code.py`, `programs.json`, `font_*.bin`, `README.md` (bundled + served as `/pm_k1_circuitpy.zip`) | | `build.sh` | resolve markers → self‑contained `dist/` pages (+ `pico-main.py`) | | `deploy.sh` | build, then publish to the Caddy web root | | `release.sh` | tag a formal version | diff --git a/editor.html b/editor.html index 85bbe3d..d9f4ce4 100644 --- a/editor.html +++ b/editor.html @@ -272,7 +272,7 @@
 
-
+
@@ -1115,6 +1115,29 @@ async function loadFromDevice() { inp.click(); } +/* Device audio (Phase 3): a connected PM_K-1 sends a USB-MIDI note per click; we voice it through + this page's synth, so the device drives sound out the computer's speakers, locked to its clock. */ +let _midiAccess = null, _midiOn = false; +function onDeviceMidi(e) { + const d = e.data; if (!d || d.length < 3) return; + if ((d[0] & 0xf0) === 0x90 && d[2] > 0) { // Note On + const v = d[2], gain = v >= 110 ? 1.0 : v >= 70 ? 0.6 : 0.25; // accent / normal / ghost + try { playInstrument(GM_NUM[d[1]] || "beep", audioCtx.currentTime, gain); } catch (_) {} + } +} +function _bindMidi() { if (_midiAccess) for (const inp of _midiAccess.inputs.values()) inp.onmidimessage = _midiOn ? onDeviceMidi : null; } +function updateMidiBtn() { const b = $("midiBtn"); if (b) { b.textContent = _midiOn ? "🎹 Device audio: ON" : "🎹 Device audio"; b.classList.toggle("primary", _midiOn); } } +async function toggleDeviceAudio() { + if (_midiOn) { _midiOn = false; _bindMidi(); updateMidiBtn(); return; } + if (!navigator.requestMIDIAccess) return alert("Playing the device through this computer needs the Web MIDI API — use Chrome or Edge."); + try { if (!_midiAccess) { _midiAccess = await navigator.requestMIDIAccess(); _midiAccess.onstatechange = _bindMidi; } } + catch (e) { return alert("MIDI access was denied."); } + ensureAudio(); if (audioCtx && audioCtx.state === "suspended") audioCtx.resume(); + _midiOn = true; _bindMidi(); updateMidiBtn(); + if (![..._midiAccess.inputs.values()].length) + alert("No MIDI device detected yet — plug in the PM_K-1 (CircuitPython firmware) and press play on it. (It stays armed; new devices connect automatically.)"); +} + // Apply a shared link on load. Returns true if it set the metronome state. function applyHashShare() { const h = location.hash || ""; @@ -1340,6 +1363,7 @@ $("importBtn").addEventListener("click", () => { $("trayMenu").hidden = true; $( $("importFile").addEventListener("change", (e) => { if (e.target.files[0]) importAll(e.target.files[0]); e.target.value = ""; }); $("saveDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true; saveToDevice(); }); $("loadDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true; loadFromDevice(); }); +$("midiBtn").addEventListener("click", toggleDeviceAudio); $("clearLogBtn").addEventListener("click", () => { $("trayMenu").hidden = true; clearLog(); }); $("resetAllBtn").addEventListener("click", () => { $("trayMenu").hidden = true; resetAll(); }); $("shareSettingsBtn").addEventListener("click", () => { $("trayMenu").hidden = true; shareSettings(); }); diff --git a/info-kit.html b/info-kit.html index 1286a4b..c939e49 100644 --- a/info-kit.html +++ b/info-kit.html @@ -135,8 +135,9 @@

An alternative firmware that makes the Pico mount as a USB drive carrying the firmware, your tracks (programs.json) and a copy of this editor — design grooves on the web and write them straight to the device, no Thonny. A full lanes/pads display with anti‑aliased - text drives the touchscreen. The MicroPython firmware above stays the simple, rock‑solid option. - (USB‑MIDI audio out to your computer's speakers is the next step.)

+ text drives the touchscreen, and it plays out your computer's speakers over USB‑MIDI (the + editor's 🎹 Device audio button voices it, locked to the device clock). The MicroPython + firmware above stays the simple, rock‑solid option.

Download CircuitPython bundle ↓ Source + README ↗ @@ -149,6 +150,8 @@ set‑list menu → 📟 Save to device → pick the CIRCUITPY drive. The Pico auto‑reloads with your grooves. (In Chrome/Edge it writes straight to the drive; otherwise it downloads programs.json to drag on.) 📥 Load from device reads it back. +

  • Play through your computer: in the editor (Chrome/Edge) click 🎹 Device audio, then + press play on the device — its full groove sounds through your speakers over USB‑MIDI, in sync.
  • diff --git a/pico-cp/README.md b/pico-cp/README.md index 9340e0f..ffd4f56 100644 --- a/pico-cp/README.md +++ b/pico-cp/README.md @@ -23,6 +23,14 @@ same program‑string language as . 3. It starts immediately. Editing `programs.json` (or re‑saving it from the editor) makes CircuitPython **auto‑reload** with the new tracks. +## Play through the computer's speakers (USB-MIDI) + +The board also shows up as a **USB-MIDI** device and sends a note on every click (a GM drum note per +lane, velocity by accent). Open the [editor](https://metronome.varasys.io/editor.html) in **Chrome/Edge**, +click **🎹 Device audio**, grant MIDI access, then press play *on the device* — the editor voices the +groove through its full synth, out your computer's speakers, locked to the device's clock. Set +`MUTE_BUZZER = True` in `code.py` if you'd rather silence the on-board buzzer while doing this. + ## Controls (same as the MicroPython build) - **Touch:** on‑screen `◀◀ / ▶ / ▶▶` (prev · play/stop · next) and `− / TAP / +`. @@ -51,6 +59,7 @@ on. **📥 Load from device** reads a `programs.json` back into a new set list. - **Taps land wrong:** set `TOUCH_DEBUG = True`, watch the serial output, then set `TOUCH_SWAP_XY` / `TOUCH_INVERT_X` / `TOUCH_INVERT_Y`. - **Joystick reversed:** toggle `JOY_INVERT_X` / `JOY_INVERT_Y`. +- **Computer audio:** `MIDI_ENABLED` (default on) sends the MIDI notes; `MUTE_BUZZER` silences the buzzer. - **LED too bright / too dim:** change `LED_BRIGHTNESS` (0..1, default 0.15). - **Screen tearing:** the SPI panel has no tearing-effect sync; `SPI_BAUD` (default 62.5 MHz) is pushed fast to minimise it — lower it only if the display is unstable. diff --git a/pico-cp/__pycache__/code.cpython-312.pyc b/pico-cp/__pycache__/code.cpython-312.pyc index 0d95bcd3f52f48138f374e40d37a3d678ad2ed61..736f66374bc6089b858ad70a445589538135939a 100644 GIT binary patch delta 9573 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 ZT11RDRb@)AWr&E`rm7XvYs*Ez{{u`?K;8fV delta 8103 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 C?=nOH diff --git a/pico-cp/code.py b/pico-cp/code.py index 04ce3e5..3b42d0f 100644 --- a/pico-cp/code.py +++ b/pico-cp/code.py @@ -4,8 +4,9 @@ # # WHY CIRCUITPYTHON: the board then mounts as a USB drive (CIRCUITPY) carrying this code, your # tracks (programs.json) and a copy of the editor — edit on the web, "Save to device" writes -# programs.json here, and CircuitPython auto-reloads with the new grooves. (USB-MIDI audio out -# to the computer comes in a later phase.) Runs the SAME program strings as metronome.varasys.io. +# programs.json here, and CircuitPython auto-reloads with the new grooves. It also sends USB-MIDI +# (a note per click) so the web editor can play it out the computer's speakers ("Device audio"). +# Runs the SAME program strings as metronome.varasys.io. # # INSTALL: flash CircuitPython (https://circuitpython.org/board/raspberry_pi_pico/), then copy # this file as code.py plus programs.json onto the CIRCUITPY drive. It runs on boot. @@ -26,10 +27,16 @@ try: import neopixel_write # core module on RP2040 — drives WS2812 with no external library except ImportError: neopixel_write = None +try: + import usb_midi # default-enabled on RP2040 — sends a MIDI note per click to the computer +except ImportError: + usb_midi = None # ============================== CONFIG (tweak if needed) ============================== SPI_BAUD = 62_500_000 # faster SPI = smaller tearing window; drop to 40_000_000 if unstable LED_BRIGHTNESS = 0.15 # WS2812 sits right next to you — keep it dim (0..1) +MIDI_ENABLED = True # send a USB-MIDI note per click (play via the web editor's "Device audio") +MUTE_BUZZER = False # silence the on-board buzzer (e.g. when using computer audio) WIDTH, HEIGHT = 320, 480 MADCTL = 0x48 # portrait; 0x48 swaps R/B for this BGR panel (cyan reads cyan). Use 0x40 if reversed. INVERT_COLORS = True # most ST7796 modules need inversion ON; set False if colours look negative @@ -63,6 +70,14 @@ 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)} +# voice -> General-MIDI note (USB-MIDI bridge), and level -> MIDI velocity +SOUND_GM = {"kick":36,"kick808":36,"kick909":36, "snare":38,"snare808":38,"snare909":38, + "clap":39,"clap808":39,"clap909":39, "rim":37, "hatClosed":42,"hat808":42,"hat909":42, + "hatOpen":46,"openHat808":46, "ride":51,"ride909":51, "crash":49,"crash909":49, + "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 +MIDI_VEL = {2: 120, 1: 90, 3: 45} # accent / normal / ghost 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 @@ -273,6 +288,7 @@ class App: self.display = make_display() self.i2c = busio.I2C(scl=P_SCL, sda=P_SDA, frequency=400_000) self.touch = GT911(self.i2c) + self.midi = usb_midi.ports[1] if (MIDI_ENABLED and usb_midi and len(usb_midi.ports) > 1) else None self.led = RGB(P_RGB) self.buz = pwmio.PWMOut(P_BUZ, frequency=1600, variable_frequency=True, duty_cycle=0) self.buz_off = 0 @@ -357,6 +373,10 @@ class App: def led_off(self): self.rgb = (0, 0, 0) self.led.set(0, 0, 0) + def midi_send(self, note, vel): # device-as-conductor: a note per click to the computer + if self.midi is None: return + try: self.midi.write(bytes([0x90, note, vel])) # Note On (percussive — no Note Off needed) + except Exception: pass # ---------- transport ---------- def toggle(self): @@ -393,11 +413,15 @@ class App: 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 lvl > 0: + fired.append(lvl) + self.midi_send(SOUND_GM.get(L['sound'], GM_DEFAULT), MIDI_VEL.get(lvl, 90)) # one note per lane 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) + best = max(fired, key=lambda l: PRIO.get(l, 0)) + if not MUTE_BUZZER: self.click(best) + self.flash(best) 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)