auto-detect MIDI device, drop ALSA sequencer dependency, MPK Mini simulator
midi-bridge.py: - Auto-detect rawmidi device (gadget f_midi > env override > fallback) - Reconnect on ENODEV (device hotplug) - No longer needs MIDI_BRIDGE_DEV env var in common cases start-synth.sh: - Remove all -alsamidi/-midiadddev flags (kernel lacks CONFIG_SND_SEQUENCER) - MIDI handled entirely by midi-bridge.service → UDP → netreceive - Host mode: same approach, no ALSA sequencer needed mpk-mini-sim.pd: - New MPK Mini Mk II simulator (25 keys, 8 pads, 8 CC knobs) - Proper Pd #X obj format for all objects (bng, hsl, t, del, netsend, loadbang) - UDP output to board:8081, matching midi-bridge format system: enable usb-gadget-audio.service at boot, load dwc3 module at boot
This commit is contained in:
+65
-39
@@ -1,76 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bridge raw MIDI device to Pd via UDP. For host mode with MPK Mini.
|
||||
"""Bridge raw MIDI device to Pd via UDP. Auto-detects gadget vs host mode.
|
||||
|
||||
Sends parsed MIDI messages as Pd-style 'midi status data1 data2;' strings
|
||||
to UDP ports for Pd (8081) and LED matrix visualization (8082).
|
||||
"""
|
||||
import os, socket, time, select, struct
|
||||
import os, socket, time, select, pathlib
|
||||
|
||||
MIDI_DEV = os.environ.get("MIDI_BRIDGE_DEV", "/dev/snd/midiC0D0")
|
||||
PD_HOST = os.environ.get("MIDI_BRIDGE_HOST", "127.0.0.1")
|
||||
PD_PORT = int(os.environ.get("MIDI_BRIDGE_PORT", "8081"))
|
||||
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
|
||||
|
||||
def _find_midi_dev():
|
||||
"""Auto-detect rawmidi device. Preference: gadget f_midi > env override > host mode."""
|
||||
dev = os.environ.get("MIDI_BRIDGE_DEV")
|
||||
if dev:
|
||||
return dev
|
||||
# Look for gadget f_midi card first
|
||||
cards = pathlib.Path("/proc/asound").glob("card*")
|
||||
for card_dir in cards:
|
||||
cmi = card_dir / "id"
|
||||
if cmi.exists() and "midi" in cmi.read_text().strip():
|
||||
card_num = card_dir.name.replace("card", "")
|
||||
midi_dev = f"/dev/snd/midiC{card_num}D0"
|
||||
if os.path.exists(midi_dev):
|
||||
return midi_dev
|
||||
# Fallback: any rawmidi device
|
||||
rawdevs = sorted(pathlib.Path("/dev/snd").glob("midiC?D0"))
|
||||
if rawdevs:
|
||||
return str(rawdevs[0])
|
||||
return "/dev/snd/midiC0D0" # fallback for host mode
|
||||
|
||||
def midi_msg(status, data1, data2=None):
|
||||
if data2 is not None:
|
||||
return f"midi {status} {data1} {data2};"
|
||||
return f"midi {status} {data1};"
|
||||
|
||||
def main():
|
||||
while not os.path.exists(MIDI_DEV):
|
||||
def _open_midi():
|
||||
midi_dev = _find_midi_dev()
|
||||
while not os.path.exists(midi_dev):
|
||||
time.sleep(0.5)
|
||||
|
||||
fd = os.open(MIDI_DEV, os.O_RDWR | os.O_NONBLOCK)
|
||||
midi_dev = _find_midi_dev()
|
||||
fd = os.open(midi_dev, os.O_RDWR | os.O_NONBLOCK)
|
||||
os.set_blocking(fd, False)
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
return fd
|
||||
|
||||
def main():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
running = True
|
||||
buf = bytearray()
|
||||
while running:
|
||||
try:
|
||||
events = poll.poll(500)
|
||||
for f, flags in events:
|
||||
if flags & select.POLLIN:
|
||||
data = os.read(fd, 1024)
|
||||
if not data:
|
||||
continue
|
||||
for byte in data:
|
||||
if byte & 0x80:
|
||||
if len(buf) > 0 and buf[0] >= 0x80:
|
||||
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
|
||||
buf = bytearray([byte])
|
||||
else:
|
||||
buf.append(byte)
|
||||
if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0):
|
||||
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
|
||||
buf = bytearray()
|
||||
elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0):
|
||||
_flush(sock, buf, PD_HOST, PD_PORT, VIZ_PORT)
|
||||
buf = bytearray()
|
||||
except (BlockingIOError, OSError) as e:
|
||||
if isinstance(e, OSError) and e.errno == 19:
|
||||
time.sleep(1)
|
||||
continue
|
||||
if not isinstance(e, BlockingIOError):
|
||||
raise
|
||||
fd = _open_midi()
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
buf = bytearray()
|
||||
while running:
|
||||
try:
|
||||
events = poll.poll(500)
|
||||
for f, flags in events:
|
||||
if flags & select.POLLIN:
|
||||
data = os.read(fd, 1024)
|
||||
if not data:
|
||||
continue
|
||||
for byte in data:
|
||||
if byte & 0x80:
|
||||
if len(buf) > 0 and buf[0] >= 0x80:
|
||||
_flush(sock, buf)
|
||||
buf = bytearray([byte])
|
||||
else:
|
||||
buf.append(byte)
|
||||
if len(buf) >= 3 and (buf[0] & 0xE0) in (0x80, 0x90, 0xA0, 0xB0, 0xE0):
|
||||
_flush(sock, buf)
|
||||
buf = bytearray()
|
||||
elif len(buf) >= 2 and (buf[0] & 0xE0) in (0xC0, 0xD0):
|
||||
_flush(sock, buf)
|
||||
buf = bytearray()
|
||||
except OSError as e:
|
||||
if e.errno == 19:
|
||||
os.close(fd)
|
||||
time.sleep(1)
|
||||
break
|
||||
raise
|
||||
except BlockingIOError:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
running = False
|
||||
|
||||
os.close(fd)
|
||||
sock.close()
|
||||
|
||||
def _flush(sock, buf, pd_host, pd_port, viz_port):
|
||||
def _flush(sock, buf):
|
||||
status = buf[0]
|
||||
if (buf[0] & 0xF0) == 0xF0:
|
||||
return
|
||||
data1 = buf[1]
|
||||
data2 = buf[2] if len(buf) >= 3 else None
|
||||
msg = midi_msg(status, data1, data2)
|
||||
sock.sendto(msg.encode(), (pd_host, pd_port))
|
||||
sock.sendto(msg.encode(), (pd_host, viz_port))
|
||||
sock.sendto(msg.encode(), (PD_HOST, PD_PORT))
|
||||
sock.sendto(msg.encode(), (PD_HOST, VIZ_PORT))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user