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:
+103
@@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# diag.sh — run on the board for diagnostics
|
||||
set -e
|
||||
|
||||
echo "--- System ---"
|
||||
echo " Kernel: $(uname -r)"
|
||||
echo " Uptime: $(uptime -p | sed 's/up //')"
|
||||
echo " Load: $(cat /proc/loadavg | cut -d' ' -f1-3)"
|
||||
echo " Memory: $(free -h | awk '/^Mem:/{print $3 "/" $2}')"
|
||||
echo " Disk: $(df -h / | awk 'NR==2{print $3 "/" $2 " (" $5 ")"}')"
|
||||
|
||||
echo ""
|
||||
echo "--- USB Gadget (configfs) ---"
|
||||
UDC=$(cat /sys/kernel/config/usb_gadget/g1/UDC 2>/dev/null || echo '(not bound)')
|
||||
echo " UDC bound: $UDC"
|
||||
for f in /sys/kernel/config/usb_gadget/g1/functions/*/; do
|
||||
echo " Function: $(basename "$f")"
|
||||
done
|
||||
for c in /sys/kernel/config/usb_gadget/g1/configs/*/; do
|
||||
cfg=$(basename "$c")
|
||||
for l in "$c"/*.*/; do
|
||||
[ -L "$l" ] && echo " linked: $(basename "$l") -> config $cfg"
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- UDC Hardware ---"
|
||||
for u in /sys/class/udc/*/; do
|
||||
name=$(basename "$u")
|
||||
echo " Controller: $name"
|
||||
echo " state: $(cat "$u/state" 2>/dev/null)"
|
||||
echo " speed: $(cat "$u/current_speed" 2>/dev/null)"
|
||||
echo " max_speed: $(cat "$u/maximum_speed" 2>/dev/null)"
|
||||
done
|
||||
role=$(cat /sys/class/usb_role/*/role 2>/dev/null || echo '(none)')
|
||||
echo " USB role: $role"
|
||||
echo " Role sw: $(ls /sys/class/usb_role/ 2>/dev/null | tr '\n' ' ')"
|
||||
|
||||
echo ""
|
||||
echo "--- USB Audio Params ---"
|
||||
for p in /sys/kernel/config/usb_gadget/g1/functions/uac1.gs0/p_* /sys/kernel/config/usb_gadget/g1/functions/uac1.gs0/c_*; do
|
||||
name=$(basename "$p")
|
||||
echo " $name: $(cat "$p" 2>/dev/null)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- USB MIDI Params ---"
|
||||
for p in in_ports out_ports index; do
|
||||
val=$(cat /sys/kernel/config/usb_gadget/g1/functions/midi.gs0/$p 2>/dev/null)
|
||||
echo " $p: $val"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- Modules ---"
|
||||
lsmod | awk '$1 ~ /^(usb_f_|u_audio|libcomposite|snd_usb|snd_rawmidi|snd_soc|dwc3)/ {print " " $0}'
|
||||
for m in usb_f_uac1 usb_f_midi dwc3-qcom-legacy; do
|
||||
if lsmod 2>/dev/null | grep -q "^$m "; then
|
||||
echo " $m: LOADED"
|
||||
else
|
||||
echo " $m: not loaded"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- ALSA Cards ---"
|
||||
cat /proc/asound/cards
|
||||
echo ""
|
||||
aplay -l 2>/dev/null | awk '{print " " $0}'
|
||||
echo ""
|
||||
arecord -l 2>/dev/null | awk '{print " " $0}'
|
||||
|
||||
echo ""
|
||||
echo "--- ALSA MIDI ---"
|
||||
echo " Raw MIDI devices:"
|
||||
ls /dev/snd/midi* 2>/dev/null | awk '{print " " $0}' || echo " (none)"
|
||||
echo " ALSA sequencer:"
|
||||
aconnect -lio 2>/dev/null | awk '{print " " $0}' || echo " (none)"
|
||||
echo " amidi ports:"
|
||||
amidi -l 2>/dev/null | awk '{print " " $0}' || echo " (none)"
|
||||
|
||||
echo ""
|
||||
echo "--- Pd Status ---"
|
||||
pd_pid=$(pgrep -x pd 2>/dev/null || echo "")
|
||||
if [ -n "$pd_pid" ]; then
|
||||
echo " PID: $pd_pid"
|
||||
echo " Args: $(tr '\0' ' ' < /proc/$pd_pid/cmdline 2>/dev/null)"
|
||||
echo " Uptime: $(ps -o etime= -p $pd_pid 2>/dev/null | xargs)"
|
||||
echo " CPU: $(ps -o %cpu= -p $pd_pid 2>/dev/null | xargs)%"
|
||||
echo " MEM: $(ps -o %mem= -p $pd_pid 2>/dev/null | xargs)%"
|
||||
else
|
||||
echo " Pd: not running"
|
||||
fi
|
||||
for svc in pd-synth-auto pd-synth pd-synth-i2s usb-role-detect usb-gadget-audio; do
|
||||
state=$(systemctl is-active "$svc" 2>/dev/null || echo '(not found)')
|
||||
enabled=$(systemctl is-enabled "$svc" 2>/dev/null || echo '(not found)')
|
||||
echo " $svc: $state (enabled: $enabled)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- dmesg (USB/gadget errors) ---"
|
||||
dmesg -l err,warn 2>/dev/null | grep -iE 'usb|gadget|uac1|midi|pd|audio|dwc3' | tail -20 | awk '{print " " $0}'
|
||||
echo ""
|
||||
dmesg 2>/dev/null | grep -i 'failed to start\|incorrect sample\|couldn.t find.*UDC\|probe.*failed' | tail -5 | awk '{print " " $0}'
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Listen for MIDI events via UDP and display note activity on LED matrix.
|
||||
|
||||
Uses RPC requests (not notifications) for reliability, with rate limiting.
|
||||
"""
|
||||
import socket, msgpack, time, os, re
|
||||
|
||||
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
|
||||
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
|
||||
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8"))
|
||||
MIN_INTERVAL = 1 / 15
|
||||
|
||||
COLS = 13
|
||||
ROWS = 8
|
||||
|
||||
def pack_frame(pixels):
|
||||
bits = 0
|
||||
for row in range(ROWS):
|
||||
for col in range(COLS):
|
||||
if pixels[row][col]:
|
||||
bits |= 1 << (row * COLS + col)
|
||||
return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF,
|
||||
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF)
|
||||
|
||||
def main():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("0.0.0.0", VIZ_PORT))
|
||||
sock.setblocking(False)
|
||||
|
||||
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
router.settimeout(2)
|
||||
router.connect(ROUTER_SOCK)
|
||||
packer = msgpack.Packer()
|
||||
|
||||
active_notes = {}
|
||||
last_frame = None
|
||||
last_send = 0
|
||||
buf = b""
|
||||
msg_id = 0
|
||||
|
||||
while True:
|
||||
now = time.monotonic()
|
||||
|
||||
try:
|
||||
while True:
|
||||
data, _ = sock.recvfrom(1024)
|
||||
buf += data
|
||||
except BlockingIOError:
|
||||
pass
|
||||
|
||||
while b";" in buf:
|
||||
msg, buf = buf.split(b";", 1)
|
||||
msg = msg.decode().strip()
|
||||
parts = msg.split()
|
||||
if len(parts) >= 3 and parts[0] == "midi":
|
||||
status = int(parts[1])
|
||||
note = int(parts[2])
|
||||
vel = int(parts[3]) if len(parts) >= 4 else 0
|
||||
msg_type = status & 0xF0
|
||||
if msg_type == 0x90 and vel > 0:
|
||||
active_notes[note] = DECAY_FRAMES
|
||||
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
|
||||
active_notes.pop(note, None)
|
||||
|
||||
if now - last_send >= MIN_INTERVAL:
|
||||
pixels = [[0] * COLS for _ in range(ROWS)]
|
||||
for note, decay in list(active_notes.items()):
|
||||
if decay <= 0:
|
||||
del active_notes[note]
|
||||
continue
|
||||
active_notes[note] = decay - 1
|
||||
note_min, note_max = 36, 96
|
||||
row = 7 - int((note - note_min) / (note_max - note_min) * 7)
|
||||
row = max(0, min(7, row))
|
||||
col = note % 12
|
||||
pixels[row][col] = 1
|
||||
|
||||
frame = pack_frame(pixels)
|
||||
if frame != last_frame:
|
||||
msg_id = (msg_id + 1) % 65535
|
||||
params = [str(v) for v in frame]
|
||||
req = packer.pack([0, msg_id, "draw_frame", params])
|
||||
try:
|
||||
router.sendall(req)
|
||||
router.settimeout(0.3)
|
||||
router.recv(128)
|
||||
router.settimeout(2)
|
||||
except socket.timeout:
|
||||
pass
|
||||
except OSError:
|
||||
router.close()
|
||||
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
router.settimeout(2)
|
||||
router.connect(ROUTER_SOCK)
|
||||
last_frame = frame
|
||||
last_send = now
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PD_DIR="/home/arduino/pd"
|
||||
PATCH="$PD_DIR/synth.pd"
|
||||
MODE="usb"
|
||||
VERBOSE=false
|
||||
|
||||
die() { echo "[ERROR] $*" >&2; exit 1; }
|
||||
info() { echo "[INFO] $*"; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-a|--auto) MODE="auto"; shift ;;
|
||||
-i|--i2s) MODE="i2s"; shift ;;
|
||||
-l|--loopback) MODE="loopback"; shift ;;
|
||||
-v|--verbose) VERBOSE=true; shift ;;
|
||||
-k|--kill) pkill -x pd 2>/dev/null && info "Pd stopped" || info "Pd not running"; exit 0 ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v pd &>/dev/null; then
|
||||
die "Pure Data not found. Install: apt install puredata"
|
||||
fi
|
||||
|
||||
if [ ! -f "$PATCH" ]; then
|
||||
die "Patch not found: $PATCH"
|
||||
fi
|
||||
|
||||
# --- Auto-detect mode ---
|
||||
if [ "$MODE" = "auto" ]; then
|
||||
if [ -f /run/usb-role ]; then
|
||||
ROLE=$(cat /run/usb-role)
|
||||
else
|
||||
ROLE=$(/usr/local/bin/detect-usb-role.sh 2>/dev/null && cat /run/usb-role || echo "gadget")
|
||||
fi
|
||||
|
||||
case "$ROLE" in
|
||||
gadget)
|
||||
MODE="usb"
|
||||
if ! aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then
|
||||
info "Configuring USB gadget..."
|
||||
/usr/local/bin/configure-usb-audio.sh start 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
;;
|
||||
host)
|
||||
if [ -f /run/usb-audio-dev ] && [ -s /run/usb-audio-dev ]; then
|
||||
ALSA_DEV=$(cat /run/usb-audio-dev)
|
||||
else
|
||||
info "Detecting USB audio interface..."
|
||||
/usr/local/bin/configure-usb-host.sh 2>/dev/null || {
|
||||
die "No USB audio interface found. Plug one in via hub."
|
||||
}
|
||||
ALSA_DEV=$(cat /run/usb-audio-dev)
|
||||
fi
|
||||
info "Mode: USB host (MIDI controller + USB audio interface)"
|
||||
MIDI_OPTS=(-alsamidi)
|
||||
midi_dev=$(amidi -l 2>/dev/null | grep -v "^Dir" | head -1 | awk '{print $2}' | sed 's/\(hw:[0-9]*,[0-9]*\).*/\1/')
|
||||
if [ -n "$midi_dev" ]; then
|
||||
MIDI_OPTS+=(-midiadddev "$midi_dev")
|
||||
info " MIDI device: $midi_dev"
|
||||
fi
|
||||
AUDIO_OPTS=(-audiooutdev 4 -audioindev 4)
|
||||
PD_ARGS=(
|
||||
-nogui -alsa
|
||||
-send "pd dsp 1"
|
||||
"${MIDI_OPTS[@]}"
|
||||
"${AUDIO_OPTS[@]}"
|
||||
-audiobuf 20 -blocksize 64 -r 48000
|
||||
-channels 2 -inchannels 0 -outchannels 2
|
||||
-path "$PD_DIR" "$PATCH"
|
||||
)
|
||||
if [ "$VERBOSE" = true ]; then pd "${PD_ARGS[@]}"
|
||||
else pd "${PD_ARGS[@]}" > /dev/null 2>&1 &
|
||||
PID=$!; info "Pd started (PID: $PID)"; info " Stop: kill $PID"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
info "Auto-detect: unknown role '$ROLE', falling back to gadget"
|
||||
MODE="usb"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- Configure per-mode options ---
|
||||
AUDIO_OPTS=()
|
||||
MIDI_OPTS=(-alsamidi)
|
||||
|
||||
if [ "$MODE" = "usb" ]; then
|
||||
info "Mode: USB gadget (UAC1 audio + gmidi MIDI)"
|
||||
AUDIO_OPTS=(-audiooutdev 2 -audioindev 2)
|
||||
if aplay -l 2>/dev/null | grep -q "UAC1Gadget"; then
|
||||
info " USB gadget: OK"
|
||||
midi_dev=$(amidi -l 2>/dev/null | grep "f_midi" | head -1 | awk '{print $2}')
|
||||
if [ -n "$midi_dev" ]; then
|
||||
MIDI_OPTS+=(-midiadddev "$midi_dev")
|
||||
info " MIDI device: $midi_dev"
|
||||
fi
|
||||
else
|
||||
info " USB gadget: NOT DETECTED (kernel modules needed)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "i2s" ]; then
|
||||
if [ -f /run/i2s-audio-dev ] && [ -s /run/i2s-audio-dev ]; then
|
||||
ALSA_DEV=$(cat /run/i2s-audio-dev)
|
||||
else
|
||||
for dev in "hw:I2S,0" "hw:MI2S,0" "hw:0,0"; do
|
||||
if aplay -l 2>/dev/null | grep -qi "${dev%,*}"; then
|
||||
ALSA_DEV="$dev"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
info "Mode: I2S DAC (standalone analog audio)"
|
||||
AUDIO_OPTS=(-audioadddev "$ALSA_DEV")
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "loopback" ]; then
|
||||
info "Mode: ALSA loopback (testing)"
|
||||
AUDIO_OPTS=(-audiodev 2)
|
||||
fi
|
||||
|
||||
PD_ARGS=(
|
||||
-nogui
|
||||
-alsa
|
||||
"${MIDI_OPTS[@]}"
|
||||
"${AUDIO_OPTS[@]}"
|
||||
-send "pd dsp 1"
|
||||
-audiobuf 20
|
||||
-blocksize 64
|
||||
-r 48000
|
||||
-channels 1
|
||||
-inchannels 0
|
||||
-outchannels 1
|
||||
-path "$PD_DIR"
|
||||
"$PATCH"
|
||||
)
|
||||
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
pd "${PD_ARGS[@]}" &
|
||||
else
|
||||
pd "${PD_ARGS[@]}" > /dev/null 2>&1 &
|
||||
fi
|
||||
PID=$!
|
||||
info "Pd started (PID: $PID)"
|
||||
info " Stop: kill $PID"
|
||||
wait $PID
|
||||
info "Pd exited (code: $?)"
|
||||
Reference in New Issue
Block a user