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) <noreply@anthropic.com>
This commit is contained in:
parent
6b508ab86e
commit
cc56741483
3 changed files with 25 additions and 4 deletions
12
editor.html
12
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."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue