diff --git a/scripts/led-matrix-viz.py b/scripts/led-matrix-viz.py index 4dd797a..b4248dd 100644 --- a/scripts/led-matrix-viz.py +++ b/scripts/led-matrix-viz.py @@ -1,18 +1,19 @@ #!/usr/bin/env python3 """Listen for MIDI events via UDP and display note activity on LED matrix. -Uses select() for polling both UDP and router RPC responses. +Non-blocking I/O with immediate MIDI processing and fast frame rendering. """ -import socket, msgpack, time, os, select +import socket, msgpack, time, os, select, collections 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")) -FPS = 15 +FPS = 30 COLS = 13 ROWS = 8 + def pack_frame(pixels): bits = 0 for row in range(ROWS): @@ -22,15 +23,24 @@ def pack_frame(pixels): return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF, (bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF) + +def connect_router(): + router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + router.setblocking(False) + try: + router.connect(ROUTER_SOCK) + except OSError: + pass + return router + + 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.setblocking(False) - router.connect(ROUTER_SOCK) + router = connect_router() packer = msgpack.Packer() active_notes = {} @@ -40,52 +50,65 @@ def main(): frame_interval = 1.0 / FPS next_frame = time.monotonic() router_buf = b"" + frames_sent = 0 + frames_dropped = 0 poller = select.poll() poller.register(sock, select.POLLIN) - poller.register(router, select.POLLIN) while True: + # Always poll UDP with short timeout timeout = max(0, int((next_frame - time.monotonic()) * 1000)) + timeout = min(timeout, 10) # cap at 10ms for responsiveness + try: events = poller.poll(timeout) except OSError: events = [] + # Process all pending UDP data immediately for fd, flags in events: if fd == sock.fileno() and flags & select.POLLIN: try: - data, _ = sock.recvfrom(1024) - buf += data - 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) + while True: + data, _ = sock.recvfrom(1024) + buf += data except BlockingIOError: pass - if fd == router.fileno() and flags & select.POLLIN: - try: + # Parse all complete messages + 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) + + # Also drain any pending router responses + if router.fileno() >= 0: + try: + while True: router_buf += router.recv(4096) - except BlockingIOError: - pass + except BlockingIOError: + pass - if time.monotonic() >= next_frame: - next_frame = time.monotonic() + frame_interval + # Render frame when it's time + now = time.monotonic() + if now >= next_frame: + next_frame = now + frame_interval pixels = [[0] * COLS for _ in range(ROWS)] - for note, decay in list(active_notes.items()): + expired = [] + for note, decay in active_notes.items(): if decay <= 0: - del active_notes[note] + expired.append(note) continue active_notes[note] = decay - 1 note_min, note_max = 36, 96 @@ -94,6 +117,9 @@ def main(): col = note % 12 pixels[row][col] = 1 + for note in expired: + del active_notes[note] + frame = pack_frame(pixels) if frame != last_frame: msg_id = (msg_id + 1) % 65535 @@ -101,9 +127,17 @@ def main(): req = packer.pack([0, msg_id, "draw_frame", params]) try: router.sendall(req) - except BlockingIOError: - pass + frames_sent += 1 + except (BlockingIOError, OSError): + frames_dropped += 1 + # Reconnect on error + try: + router.close() + except OSError: + pass + router = connect_router() last_frame = frame + if __name__ == "__main__": main()