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:
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate mpk-mini-sim.pd — MPK Mini Mk II simulator.
|
||||
|
||||
Architecture: each key/pad has bng→t b b→msg for note-on, del→msg for note-off.
|
||||
All msgs connect to a single [netsend -u -b] via send/receive.
|
||||
Knobs: hsl→msg→[s $0-cc], central [r $0-cc]→netsend.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
OUTPUT = SCRIPT_DIR.parent / "pd" / "mpk-mini-sim.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, *args))
|
||||
return idx
|
||||
|
||||
def gen():
|
||||
o, c = [], []
|
||||
|
||||
# ── UDP sender (hardcoded IP — edit the msg box to change) ──
|
||||
Obj(o, 10, 10, "loadbang")
|
||||
Obj(o, 10, 30, "msg", ";")
|
||||
msg_conn = Obj(o, 10, 50, "msg", "connect 192.168.9.167 8081;")
|
||||
|
||||
ns = Obj(o, 10, 80, "netsend", "-u", "-b")
|
||||
connect(c, msg_conn, 0, ns, 0)
|
||||
|
||||
Obj(o, 200, 30, "text", "← edit this line to change target IP:port")
|
||||
instr = Obj(o, 10, 440, "text", "Click keys to play. Knobs send CC on change. IPS (192.168.9.167:8081)")
|
||||
instr2 = Obj(o, 10, 460, "text", "MIDI ch: 144=note-on ch1, 128=note-off ch1, 176=CC ch1")
|
||||
|
||||
# ── Keyboard: C3(48) to C5(72) ──
|
||||
keys = [
|
||||
("C3",48),("C#3",49),("D3",50),("D#3",51),("E3",52),
|
||||
("F3",53),("F#3",54),("G3",55),("G#3",56),("A3",57),
|
||||
("A#3",58),("B3",59),
|
||||
("C4",60),("C#4",61),("D4",62),("D#4",63),("E4",64),
|
||||
("F4",65),("F#4",66),("G4",67),("G#4",68),("A4",69),
|
||||
("A#4",70),("B4",71),("C5",72),
|
||||
]
|
||||
|
||||
Obj(o, 20, 120, "text", "-- KEYBOARD --")
|
||||
for i, (name, note) in enumerate(keys):
|
||||
x = 30 + (i % 13) * 46
|
||||
y = 140 + (i // 13) * 60
|
||||
Obj(o, x, y, "text", name)
|
||||
b = Obj(o, x, y+14, "bng", 15, 25, 0, 1, 0, 0, "empty", f"k{note}", -2,-8,0,10,-262144,-1,-1)
|
||||
trig = Obj(o, x, y+44, "t", "b b")
|
||||
msg_on = Obj(o, x, y+64, "msg", f"midi 144 {note} 100;")
|
||||
msg_off = Obj(o, x, y+82, "msg", f"midi 128 {note} 0;")
|
||||
del300 = Obj(o, x, y+100, "del", 300)
|
||||
connect(c, b, 0, trig, 0)
|
||||
connect(c, trig, 0, msg_on, 0)
|
||||
connect(c, msg_on, 0, ns, 0)
|
||||
connect(c, trig, 1, del300, 0)
|
||||
connect(c, del300, 0, msg_off, 0)
|
||||
connect(c, msg_off, 0, ns, 0)
|
||||
|
||||
# ── Pads (C2-G2, notes 36-43) ──
|
||||
pads = [("C2",36),("C#2",37),("D2",38),("D#2",39),("E2",40),("F2",41),("F#2",42),("G2",43)]
|
||||
Obj(o, 680, 120, "text", "-- PADS --")
|
||||
for i, (name, note) in enumerate(pads):
|
||||
x = 680 + (i % 4) * 55
|
||||
y = 140 + (i // 4) * 55
|
||||
Obj(o, x, y, "text", name)
|
||||
b = Obj(o, x, y+14, "bng", 15, 15, 0, 1, 0, 0, "empty", f"p{note}", -2,-8,0,10,-262144,-1,-1)
|
||||
trig = Obj(o, x, y+32, "t", "b b")
|
||||
msg_on = Obj(o, x, y+52, "msg", f"midi 144 {note} 100;")
|
||||
msg_off = Obj(o, x, y+70, "msg", f"midi 128 {note} 0;")
|
||||
del200 = Obj(o, x, y+88, "del", 200)
|
||||
connect(c, b, 0, trig, 0)
|
||||
connect(c, trig, 0, msg_on, 0)
|
||||
connect(c, msg_on, 0, ns, 0)
|
||||
connect(c, trig, 1, del200, 0)
|
||||
connect(c, del200, 0, msg_off, 0)
|
||||
connect(c, msg_off, 0, ns, 0)
|
||||
|
||||
# ── Knobs (CC 1, 71-77) ──
|
||||
knobs = [(1,"Mod",64),(71,"Reson",64),(72,"Release",64),(73,"Attack",64),
|
||||
(74,"Cutoff",100),(75,"Decay",64),(76,"LFO Rate",64),(77,"LFO Depth",64)]
|
||||
Obj(o, 20, 280, "text", "-- KNOBS --")
|
||||
for i, (cc, name, init) in enumerate(knobs):
|
||||
x = 30 + i * 85
|
||||
y = 300
|
||||
Obj(o, x, y, "text", f"CC{cc} {name}")
|
||||
hsl = Obj(o, x, y+15, "hsl", 50, 10, 0, 127, init, 0, 0, "empty", f"cc{cc}", -2,-8,0,10,-262144,-1,-1,7200,1)
|
||||
msg = Obj(o, x, y+30, "msg", f"midi 176 {cc} $1;")
|
||||
connect(c, hsl, 0, msg, 0)
|
||||
connect(c, msg, 0, ns, 0)
|
||||
|
||||
# ── Write (Pd format: #X obj for objects, #X msg/text/floatatom/symbol for others) ──
|
||||
lines = ["#N canvas 0 0 950 490 10;"]
|
||||
obj_types = {"bng","t","del","netsend","loadbang","hsl","comment"}
|
||||
for x, y, *args in o:
|
||||
etype = args[0]
|
||||
rest = args[1:]
|
||||
if etype in obj_types:
|
||||
# Pd stores GUI objects as: #X obj x y <name> <args>
|
||||
name = etype
|
||||
if name == "comment": name = "text" # Pd uses text for comments
|
||||
joined = " ".join(str(a) for a in rest)
|
||||
lines.append(f"#X obj {x} {y} {name} {joined};")
|
||||
else:
|
||||
joined = " ".join(str(a) for a in rest)
|
||||
lines.append(f"#X {etype} {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(f" {len(content.splitlines())} lines, {content.count('#X obj')} objects, {content.count('#X connect')} connections")
|
||||
+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()
|
||||
|
||||
+7
-25
@@ -56,17 +56,11 @@ if [ "$MODE" = "auto" ]; then
|
||||
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
|
||||
# MIDI handled by midi-bridge.service → UDP → Pd netreceive
|
||||
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
|
||||
@@ -87,18 +81,13 @@ 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
|
||||
# MIDI handled by midi-bridge.service → UDP → Pd netreceive
|
||||
else
|
||||
info " USB gadget: NOT DETECTED (kernel modules needed)"
|
||||
fi
|
||||
@@ -125,19 +114,12 @@ if [ "$MODE" = "loopback" ]; then
|
||||
fi
|
||||
|
||||
PD_ARGS=(
|
||||
-nogui
|
||||
-alsa
|
||||
"${MIDI_OPTS[@]}"
|
||||
"${AUDIO_OPTS[@]}"
|
||||
-nogui -alsa
|
||||
-send "pd dsp 1"
|
||||
-audiobuf 20
|
||||
-blocksize 64
|
||||
-r 48000
|
||||
-channels 1
|
||||
-inchannels 0
|
||||
-outchannels 1
|
||||
-path "$PD_DIR"
|
||||
"$PATCH"
|
||||
"${AUDIO_OPTS[@]}"
|
||||
-audiobuf 20 -blocksize 64 -r 48000
|
||||
-channels 1 -inchannels 0 -outchannels 1
|
||||
-path "$PD_DIR" "$PATCH"
|
||||
)
|
||||
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
|
||||
Reference in New Issue
Block a user