From cc567414834ebce62a48cfd0c8c4c059ca523df1 Mon Sep 17 00:00:00 2001 From: Me Here Date: Thu, 28 May 2026 23:38:53 -0500 Subject: [PATCH] PM_K-1: on-screen MIDI indicator + auto-mute buzzer when a host is listening The editor's 'Device audio' now sends a MIDI Active-Sensing heartbeat (0xFE, every 250ms) to the device while on. The firmware reads usb_midi.ports[0]; while it hears the heartbeat (<1s) it shows a green 'MIDI' badge top-right and silences the buzzer (the computer plays); ~1s after it stops, it reverts to the buzzer and hides the badge. Manual MUTE_BUZZER still works. Verified headless: host detected -> MIDI shown + buzzer duty 0; timeout -> reverts. Co-Authored-By: Claude Opus 4.7 (1M context) --- editor.html | 12 +++++++++--- pico-cp/__pycache__/code.cpython-312.pyc | Bin 37635 -> 39094 bytes pico-cp/code.py | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/editor.html b/editor.html index 83ca38a..e82f8d7 100644 --- a/editor.html +++ b/editor.html @@ -1117,8 +1117,14 @@ async function loadFromDevice() { /* 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, _midiFlash = 0; +let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0; function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; } +function _heartbeat(on) { // tell the device a host is listening, so it shows "MIDI" + mutes its buzzer + clearInterval(_midiBeat); _midiBeat = 0; + if (on) _midiBeat = setInterval(() => { + if (_midiAccess) for (const out of _midiAccess.outputs.values()) { try { out.send([0xFE]); } catch (_) {} } // Active Sensing + }, 250); +} function onDeviceMidi(e) { const d = e.data; if (!d || d.length < 3) return; if ((d[0] & 0xf0) === 0x90 && d[2] > 0) { // Note On @@ -1136,12 +1142,12 @@ function updateMidiBtn() { b.classList.add("primary"); } async function toggleDeviceAudio() { - if (_midiOn) { _midiOn = false; _bindMidi(); updateMidiBtn(); return; } + if (_midiOn) { _midiOn = false; _heartbeat(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(); updateMidiBtn(); }; } } catch (e) { return alert("MIDI access was denied."); } ensureAudio(); if (audioCtx && audioCtx.state === "suspended") audioCtx.resume(); - _midiOn = true; _bindMidi(); updateMidiBtn(); + _midiOn = true; _heartbeat(true); _bindMidi(); updateMidiBtn(); const names = _midiInputs().map((i) => i.name || "MIDI"); alert(names.length ? "Device audio ON.\nMIDI input(s): " + names.join(", ") + "\nPress play on the device — the button pulses green on each note received." diff --git a/pico-cp/__pycache__/code.cpython-312.pyc b/pico-cp/__pycache__/code.cpython-312.pyc index 19a9796a259b32204672f73c4fd82aacc08a4290..9b95a0147b4d9dbfeb1af0914575bcbf940b84a7 100644 GIT binary patch delta 4518 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 1) else None + self.midi_in = usb_midi.ports[0] if (MIDI_ENABLED and usb_midi and len(usb_midi.ports) > 0) else None + self._mbuf = bytearray(16); self.midi_host = False; self.last_midi_in = 0.0 self.led = RGB(P_RGB) self.buz = pwmio.PWMOut(P_BUZ, frequency=1600, variable_frequency=True, duty_cycle=0) self.buz_off = 0 @@ -321,6 +323,7 @@ class App: 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_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 × 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] @@ -426,7 +429,7 @@ class App: 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)) - if not MUTE_BUZZER: self.click(best) + if not MUTE_BUZZER and not self.midi_host: self.click(best) # computer plays it instead 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 @@ -461,6 +464,16 @@ class App: self._touchDown = True; self.hit(pt[0], pt[1]) elif self._touchDown and (nowms - self._touchSeen) > 0.14: self._touchDown = False + # MIDI host present? the editor sends a heartbeat (Active Sensing) while "Device audio" is on + if self.midi_in is not None: + try: + if self.midi_in.readinto(self._mbuf): self.last_midi_in = nowms + except Exception: pass + host = bool(self.last_midi_in) and (nowms - self.last_midi_in) < 1.0 + 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() def hit(self, x, y): for bx, by, bw, bh, key in self.buttons: @@ -481,6 +494,8 @@ class App: 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) # ---------- pad grid (each lane = a row of step pads; playhead lit as it plays) ---------- def _padbase(self, L, s):