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):