-
feat(bridge): midiviz — the MIDI stream as a picture, not a transcript · 30a704ec
PLN, 2026-08-29: "midiwatch still naively verbose [`14:0 Control change ch0, controller 29, value 15`] improve it massively. no more console, tiny borderless window that exits on q/exit/ctrl+c/alt+f4, that renders the stream in a nicer, cyberpunk matrix dense cryptic purple-on-dark, style. no word like 'control change' pure viz" The complaint is not about verbosity, it is about REGISTER. One line per event is the right shape for a debugger and the wrong shape for a performer: a fader sweep emits ~400 events a second, so midimon's prose scrolls past faster than an eye can land on it, and the one question you actually have mid-set — "which orbit did I just touch" — is the one thing a sentence makes you decode. So this is a lens, not a log. The picture ----------- Zero English words render. An event's identity is its POSITION, its type is a GLYPH CLASS, its value is a BAR plus two hex digits, and its recency is BRIGHTNESS. The layout is the authored surface itself — lcxl_grid's rows A..F in PHYSICAL_ORDER down, columns 1..8 across, column N *is* orbit N — so the answer is spatial and needs no decoding. A per-orbit charge glow behind each column says "this one is live"; the column digit brightens with it. Buttons render as latches (filled above half, hollow below) because rows E/F are gates, not levels — a bar would have lied about what they do. Channel shifts the hue of the digits, not of the cell, so the cell keeps meaning "which control" while the tint carries "from where". Only what the grid CANNOT place — foreign CCs, notes, bend, program — falls as rain in the right-hand gutter, which makes "that came from somewhere else" a visible fact rather than a thing you read. A ribbon along the bottom scrolls the raw order of arrival, brightest at the right. Every hue stays inside the violet→magenta arc, so eight roles stay distinguishable while the window still reads as one colour. Choices worth keeping --------------------- * **Mapped-but-idle must not be black.** All 48 grid cells carry a floor tint even when nothing has ever arrived on them. "Dark = not mapped" is the reading a cockpit trains you into, and a dark cell that IS mapped reads as a hardware fault — the same trap the LED work settled. * **Toolkit: PySide6 (Qt6), already installed (6.11.2).** PyQt6 and pygame are not present; GTK4 + pycairo are, but Qt gave frameless + always-on-top hints + startSystemMove (the only way a Wayland client may reposition itself) without hand-rolling a drag. * **Two frame rates, not a busy loop.** This machine performs live audio. ~25 fps while events arrive, dropping to 5 fps two seconds after the last one, where the only motion is a slow dim pulse. Colours are a precomputed LUT (8 families x 16 decay levels), paint uses the int overloads, the CRT scanline texture is one cached pixmap blitted once, and the ribbon is bounded to exactly what fits so a frame copies nothing. * **Profiled rather than guessed.** The first version cost 30.6% of a core under a sustained 400 ev/s sweep. Stage-by-stage timing put the cost in the sheer NUMBER of drawText calls, not in pixels: gutter rain alone was 3.1 ms of an 8.9 ms frame (a 96-drop cap with a trail glyph each = up to 192 calls). Capping rain at 32 heads with a trail only on the brightest, batching the ribbon into same-colour RUNS, and 33→40 ms took the frame to 5.5 ms and the sweep to 17.7%. Idle is 1.4%. The ribbon batches only hex glyphs, because those are guaranteed to come from the monospaced face at exactly one advance — a class glyph (◆ ≈ ≡) may be served by a fallback face and would drift the ribbon off its grid. Port choice: delegated, not re-derived -------------------------------------- `midimon.resolve_port()` owns "which port", and midiviz delegates to it. Two things about it are load-bearing and were nearly re-derived wrong here: the preference order is authored, and it matches the whole `aseqdump -l` ROW rather than the CLIENT column — python-rtmidi registers the lcxl3 driver's client as 'RtMidiOut Client' and puts 'ParVagues LCXL3' on the PORT. A client-name match (which is what surface.resolve_port does, correctly, for its own purpose) falls straight through to the raw hardware: the monitor would work and show the wrong stream. Confirmed live on this rig, which has both up — the driver is 133:0 under 'RtMidiOut Client'. The local fallback copy exists only for trees whose midimon predates the helper, and a test asserts it cannot drift from the original. Launcher -------- The `midimon` KEY is kept, so the Bridge hub's and the tray's existing button open the new thing with no change on either face; midimon.py is untouched and remains the terminal fallback. `terminal: True` is dropped — midiviz draws its own window, and wrapping a GUI in konsole would have parked an empty black terminal behind it for the length of the set. That also re-enables the already-running guard, so the button is now idempotent instead of spawning a window per click. Verified -------- * `ast.parse` clean; full bridge suite 90 passed (was 78 + 12 new). * `midiviz.py --selftest`: 3 s of synthetic events — every one of the 48 authored grid cells, four channels, un-gridded CCs, notes, bend, program, aftertouch, sysex — pushed through the REAL parse_line + enrich + ingest + paint path, 140 paints, 68 grabs, 139 distinct sampled colours. It grabs the widget each tick, so the drawing code is exercised for real and offscreen, not merely constructed. * Exit paths run, not assumed: SIGINT and SIGTERM each exit 0 from a live process (q/Esc/close share that path). * Plumbing, not just pure functions: a real Reader subscribed to the resolved driver port with no error, and a second one pinned to Midi Through received 8 genuine events played in with aplaymidi — aseqdump → parse_line → enrich → drain, end to end against real ALSA. * Rendered and looked at, at 420x260 and at 2x, before and after the optimizations: identical output, which is the point of the run-length batching. Found on the way: data-less realtime messages (Start/Stop/Continue/Clock) print with no data column, and midimon's row regex requires one, so they never parse — midimon's console silently omits transport too, and midiviz has four glyphs that cannot light up. Left in midimon (one parser per concept) and pinned by a test that says what to delete when it is fixed.
PLN (Algolia) authored30a704ec
×