Initial commit: Arduino Uno Q USB Audio Synth project

- USB gadget (UAC1 + MIDI) with boot persistence
- USB host mode with auto-detection
- Pure Data synth patch with ALSA + UDP netreceive MIDI input
- MIDI rawmidi-to-UDP bridge (midi-bridge.py)
- LED matrix visualization (led-matrix-viz.py via arduino-router RPC)
- MCU sketch for LED matrix control (Arduino_RouterBridge + ArduinoGraphics)
- Conftest/OPA Rego policy validation suite for .pd files
- gen_synth_pd.py: programmatic .pd file generator
- System services: midi-bridge, led-matrix-viz, pd-synth-auto, usb-role-detect
- Justfile with deploy, enable, diagnostic recipes
- Diagnostic script (diag.sh)
This commit is contained in:
2026-06-22 22:46:36 -07:00
commit 8cd796e3dd
38 changed files with 2681 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Bridge raw MIDI device to Pd via UDP. For host mode with MPK Mini.
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
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 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):
time.sleep(0.5)
fd = os.open(MIDI_DEV, os.O_RDWR | os.O_NONBLOCK)
os.set_blocking(fd, False)
poll = select.poll()
poll.register(fd, select.POLLIN)
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
except KeyboardInterrupt:
running = False
os.close(fd)
sock.close()
def _flush(sock, buf, pd_host, pd_port, viz_port):
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))
if __name__ == "__main__":
main()