164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""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).
|
|
|
|
Writes metrics to /tmp/midi-stats.json for prometheus-exporter to collect.
|
|
"""
|
|
import os, socket, time, select, pathlib, json, threading
|
|
|
|
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"))
|
|
STATS_FILE = os.environ.get("MIDI_STATS_FILE", "/tmp/midi-stats.json")
|
|
STATS_INTERVAL = 2.0
|
|
|
|
midi_notes_on = 0
|
|
midi_notes_off = 0
|
|
midi_messages_total = 0
|
|
midi_errors = 0
|
|
last_status = 0
|
|
last_note = 0
|
|
last_velocity = 0
|
|
last_time = 0.0
|
|
_stats_lock = threading.Lock()
|
|
|
|
|
|
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
|
|
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
|
|
rawdevs = sorted(pathlib.Path("/dev/snd").glob("midiC?D0"))
|
|
if rawdevs:
|
|
return str(rawdevs[0])
|
|
return "/dev/snd/midiC0D0"
|
|
|
|
|
|
def midi_msg(status, data1, data2=None):
|
|
if data2 is not None:
|
|
return f"midi {status} {data1} {data2};"
|
|
return f"midi {status} {data1};"
|
|
|
|
|
|
def _open_midi():
|
|
midi_dev = _find_midi_dev()
|
|
while not os.path.exists(midi_dev):
|
|
time.sleep(0.5)
|
|
midi_dev = _find_midi_dev()
|
|
fd = os.open(midi_dev, os.O_RDWR | os.O_NONBLOCK)
|
|
os.set_blocking(fd, False)
|
|
return fd
|
|
|
|
|
|
def _flush(sock, buf):
|
|
global midi_notes_on, midi_notes_off, midi_messages_total, midi_errors
|
|
global last_status, last_note, last_velocity, last_time
|
|
|
|
status = buf[0]
|
|
if (buf[0] & 0xF0) == 0xF0:
|
|
return
|
|
status = (status & 0xF0) | 0
|
|
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))
|
|
|
|
with _stats_lock:
|
|
midi_messages_total += 1
|
|
last_status = status
|
|
last_note = data1
|
|
last_velocity = data2 if data2 is not None else 0
|
|
last_time = time.time()
|
|
msg_type = status & 0xF0
|
|
if msg_type == 0x90 and (data2 or 0) > 0:
|
|
midi_notes_on += 1
|
|
elif msg_type == 0x80 or (msg_type == 0x90 and (data2 or 0) == 0):
|
|
midi_notes_off += 1
|
|
|
|
|
|
def _write_stats():
|
|
try:
|
|
with open(STATS_FILE, "w") as f:
|
|
json.dump({
|
|
"midi_messages_total": midi_messages_total,
|
|
"midi_notes_on_total": midi_notes_on,
|
|
"midi_notes_off_total": midi_notes_off,
|
|
"midi_errors_total": midi_errors,
|
|
"midi_last_status": last_status,
|
|
"midi_last_note": last_note,
|
|
"midi_last_velocity": last_velocity,
|
|
"midi_last_time_unix": last_time,
|
|
}, f)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def main():
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
running = True
|
|
last_stats_write = time.monotonic()
|
|
|
|
while running:
|
|
try:
|
|
fd = _open_midi()
|
|
poll = select.poll()
|
|
poll.register(fd, select.POLLIN)
|
|
buf = bytearray()
|
|
while running:
|
|
now = time.monotonic()
|
|
|
|
# Write stats periodically
|
|
if now - last_stats_write >= STATS_INTERVAL:
|
|
with _stats_lock:
|
|
_write_stats()
|
|
last_stats_write = now
|
|
|
|
try:
|
|
events = poll.poll(100)
|
|
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()
|
|
if not events:
|
|
time.sleep(0.01)
|
|
except OSError as e:
|
|
if e.errno == 19:
|
|
os.close(fd)
|
|
time.sleep(1)
|
|
break
|
|
raise
|
|
except BlockingIOError:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
running = False
|
|
|
|
sock.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |