viz: increase FPS to 30, drain UDP aggressively

- Poll timeout capped at 10ms for responsiveness
- Drain all pending UDP data before parsing
- Auto-reconnect router on error
- FPS 15 -> 30
This commit is contained in:
2026-06-24 02:33:17 -07:00
parent caef6f04c8
commit 272a13f8d9
+66 -32
View File
@@ -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()