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
+49 -15
View File
@@ -1,18 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Listen for MIDI events via UDP and display note activity on LED matrix. """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")) VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock") ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8")) DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8"))
FPS = 15 FPS = 30
COLS = 13 COLS = 13
ROWS = 8 ROWS = 8
def pack_frame(pixels): def pack_frame(pixels):
bits = 0 bits = 0
for row in range(ROWS): for row in range(ROWS):
@@ -22,15 +23,24 @@ def pack_frame(pixels):
return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF, return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF,
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 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(): def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", VIZ_PORT)) sock.bind(("0.0.0.0", VIZ_PORT))
sock.setblocking(False) sock.setblocking(False)
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) router = connect_router()
router.setblocking(False)
router.connect(ROUTER_SOCK)
packer = msgpack.Packer() packer = msgpack.Packer()
active_notes = {} active_notes = {}
@@ -40,23 +50,33 @@ def main():
frame_interval = 1.0 / FPS frame_interval = 1.0 / FPS
next_frame = time.monotonic() next_frame = time.monotonic()
router_buf = b"" router_buf = b""
frames_sent = 0
frames_dropped = 0
poller = select.poll() poller = select.poll()
poller.register(sock, select.POLLIN) poller.register(sock, select.POLLIN)
poller.register(router, select.POLLIN)
while True: while True:
# Always poll UDP with short timeout
timeout = max(0, int((next_frame - time.monotonic()) * 1000)) timeout = max(0, int((next_frame - time.monotonic()) * 1000))
timeout = min(timeout, 10) # cap at 10ms for responsiveness
try: try:
events = poller.poll(timeout) events = poller.poll(timeout)
except OSError: except OSError:
events = [] events = []
# Process all pending UDP data immediately
for fd, flags in events: for fd, flags in events:
if fd == sock.fileno() and flags & select.POLLIN: if fd == sock.fileno() and flags & select.POLLIN:
try: try:
while True:
data, _ = sock.recvfrom(1024) data, _ = sock.recvfrom(1024)
buf += data buf += data
except BlockingIOError:
pass
# Parse all complete messages
while b";" in buf: while b";" in buf:
msg, buf = buf.split(b";", 1) msg, buf = buf.split(b";", 1)
msg = msg.decode().strip() msg = msg.decode().strip()
@@ -70,22 +90,25 @@ def main():
active_notes[note] = DECAY_FRAMES active_notes[note] = DECAY_FRAMES
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0): elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
active_notes.pop(note, None) active_notes.pop(note, None)
except BlockingIOError:
pass
if fd == router.fileno() and flags & select.POLLIN: # Also drain any pending router responses
if router.fileno() >= 0:
try: try:
while True:
router_buf += router.recv(4096) router_buf += router.recv(4096)
except BlockingIOError: except BlockingIOError:
pass pass
if time.monotonic() >= next_frame: # Render frame when it's time
next_frame = time.monotonic() + frame_interval now = time.monotonic()
if now >= next_frame:
next_frame = now + frame_interval
pixels = [[0] * COLS for _ in range(ROWS)] 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: if decay <= 0:
del active_notes[note] expired.append(note)
continue continue
active_notes[note] = decay - 1 active_notes[note] = decay - 1
note_min, note_max = 36, 96 note_min, note_max = 36, 96
@@ -94,6 +117,9 @@ def main():
col = note % 12 col = note % 12
pixels[row][col] = 1 pixels[row][col] = 1
for note in expired:
del active_notes[note]
frame = pack_frame(pixels) frame = pack_frame(pixels)
if frame != last_frame: if frame != last_frame:
msg_id = (msg_id + 1) % 65535 msg_id = (msg_id + 1) % 65535
@@ -101,9 +127,17 @@ def main():
req = packer.pack([0, msg_id, "draw_frame", params]) req = packer.pack([0, msg_id, "draw_frame", params])
try: try:
router.sendall(req) router.sendall(req)
except BlockingIOError: frames_sent += 1
except (BlockingIOError, OSError):
frames_dropped += 1
# Reconnect on error
try:
router.close()
except OSError:
pass pass
router = connect_router()
last_frame = frame last_frame = frame
if __name__ == "__main__": if __name__ == "__main__":
main() main()