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 19a9796..9b95a01 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 835bc85..1c305bb 100644 --- a/pico-cp/code.py +++ b/pico-cp/code.py @@ -289,6 +289,8 @@ class App: 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.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):