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
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Generate synth.pd programmatically."""
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
OUTPUT = SCRIPT_DIR.parent / "pd" / "synth.pd"
def connect(c, src, sout, dst, din):
c.append((src, sout, dst, din))
def obj(o, x, y, *args):
idx = len(o)
o.append((x, y, "obj", *args))
return idx
def gen():
o = [] # objects
c = [] # connections
def Obj(x, y, *args):
return obj(o, x, y, *args)
# ── ALSA MIDI input (gadget mode) ──
notein = Obj(20, 20, "notein")
pack_note = Obj(80, 50, "pack 0 0")
unpack_note = Obj(80, 80, "unpack 0 0")
s_pitch_n = Obj(20, 110, "s midi-pitch")
s_vel_n = Obj(80, 110, "s midi-vel")
connect(c, notein, 0, pack_note, 0)
connect(c, notein, 1, pack_note, 1)
connect(c, pack_note, 0, unpack_note, 0)
connect(c, unpack_note, 0, s_pitch_n, 0)
connect(c, unpack_note, 1, s_vel_n, 0)
# ── UDP MIDI input (host mode via midi-bridge) ──
net = Obj(400, 20, "netreceive -u 8081")
route_midi = Obj(400, 60, "route midi")
route_note = Obj(400, 100, "route 144 128")
unpack2_on = Obj(400, 140, "unpack 0 0")
unpack2_off = Obj(480, 140, "unpack 0 0")
s_pitch_u = Obj(400, 180, "s midi-pitch")
s_vel_u = Obj(480, 180, "s midi-vel")
connect(c, net, 0, route_midi, 0)
connect(c, route_midi, 0, route_note, 0)
connect(c, route_note, 0, unpack2_on, 0)
connect(c, route_note, 1, unpack2_off, 0)
connect(c, unpack2_on, 0, s_pitch_u, 0)
connect(c, unpack2_on, 1, s_vel_u, 0)
connect(c, unpack2_off, 0, s_pitch_u, 0)
connect(c, unpack2_off, 1, s_vel_u, 0)
# ── Synth voice ──
r_vel = Obj(20, 160, "r midi-vel")
r_pitch = Obj(20, 190, "r midi-pitch")
strip = Obj(20, 220, "stripnote")
mtof = Obj(20, 260, "mtof")
env = Obj(20, 290, "env~ 50 200 0.7 400")
osc1 = Obj(140, 260, "osc~")
mul1 = Obj(140, 290, "*~ 0.3")
osc2 = Obj(220, 260, "osc~")
mul2 = Obj(220, 290, "*~ 0.3")
sum = Obj(180, 330, "+~")
scale = Obj(180, 360, "*~ 0.5")
filt = Obj(180, 400, "lop~")
amp = Obj(180, 460, "*~")
panscale = Obj(320, 460, "*~")
dac = Obj(250, 530, "dac~")
connect(c, r_vel, 0, strip, 1)
connect(c, r_pitch, 0, strip, 0)
connect(c, strip, 0, mtof, 0)
connect(c, mtof, 0, osc1, 0)
connect(c, mtof, 0, osc2, 0)
connect(c, osc1, 0, mul1, 0)
connect(c, osc2, 0, mul2, 0)
connect(c, mul1, 0, sum, 0)
connect(c, mul2, 0, sum, 1)
connect(c, sum, 0, scale, 0)
connect(c, scale, 0, filt, 0)
connect(c, filt, 0, amp, 0)
connect(c, strip, 0, env, 0)
connect(c, env, 0, amp, 1)
connect(c, amp, 0, panscale, 0)
# ── Controls ──
cutoff = Obj(350, 330, "hsl 128 15 100 18000 10000 0 0 0 empty Cutoff -2 -8 0 10 -262144 -1 -1 7200 1")
volume = Obj(350, 430, "hsl 128 15 0 100 40 0 0 0 empty Volume -2 -8 0 10 -262144 -1 -1 7200 1")
div = Obj(350, 460, "/ 100")
connect(c, cutoff, 0, filt, 1)
connect(c, volume, 0, div, 0)
connect(c, div, 0, panscale, 1)
# ── Output ──
connect(c, panscale, 0, dac, 0)
connect(c, panscale, 0, dac, 1)
# ── Write ──
lines = ["#N canvas 0 0 900 550 10;"]
for x, y, t, *args in o:
joined = " ".join(str(a) for a in args)
lines.append(f"#X obj {x} {y} {joined};")
for src, sout, dst, din in c:
lines.append(f"#X connect {src} {sout} {dst} {din};")
lines.append("")
return "\n".join(lines)
if __name__ == "__main__":
content = gen()
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(content)
print(f"Wrote {OUTPUT}")
# Print connections for debugging
for line in content.splitlines():
if line.startswith("#X connect"):
print(f" {line}")