Commit 87a1f28a by PLN (Algolia)

docs(midiviz): the state wire — one owner, two physics, pay once

PLN asked for the rendering to show where things REST, not only what just moved,
and asked that the wire not pay any cost twice.

Written down before any of it is built, because the interesting part is not the
drawing: state has exactly one legitimate owner. The surface has no readback and
in relative mode sends deltas, so the driver's integrated value is not a cache
of something else — it is the only copy. A viewer cannot derive it, and at boot
there is nothing on the wire at all.

The design: level and edge as two channels, state as last-value-wins in one
small atomically-replaced file with a seq counter so an unchanged frame parses
and paints nothing, and dirty-cell repaint so the layer costs nothing at rest.
Explicitly NOT built: a fan-out daemon for the duplicate aseqdump subscriptions,
which buys microseconds at the price of a process and a failure mode.

Two findings worth the doc on their own. Zero on a DJF is the CENTRE detent, so
the readout wants three zones with the detent drawn, not a 0-127 bar. And the
Pulsar-style feed needs no new producer: ~/.cache/parvagues/eval-events.jsonl is
already being written, one line per ctrl+enter with the orbits it touched, and
the driver already reads it.
parent 2baef681
# midiviz: state as well as events, and the wire that carries it
PLN, 2026-09-22: *"as it maintains a state, it should be 'not all same default
state' when you look at it between two movements? like, the DJFs are at zero, in
the Low, or in the High, and this doesnt really grasp from viewing the midimon?
It should feel maybe as a separate event highlighter, could it also read events
as the tidal pulsar maybe?"* — and then: *"lets do the wire efficiently to avoid
paying mlutiple times any cost, and on the contrary, be super fast and lean via
efficient components each doing a good job."*
## The diagnosis is not a theming one
`midiviz.py` draws **events**. MIDI is edge-triggered, and the module's own
design note makes recency the third visual axis: "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."
So between two movements every cell decays to the same floor tint. A DJF resting
at zero and a DJF parked at the top are the *same picture* once the glow fades.
No palette fixes that, because an event stream cannot express rest: it carries
changes, and rest is the absence of change.
Worse, a consumer of the wire alone **cannot** know the resting value. The LCXL3
has no display or LED readback (recorded in `reference_lcxl3-oled-repaint`), and
in relative mode the surface sends deltas — `rel_delta(v) = v - 64` — so the
hardware itself does not hold an absolute position for rows B and C. At boot,
before anything is touched, there is nothing on the wire at all.
## Exactly one process can know
`lcxl3-driver` integrates those deltas and OWNS the resulting value for all 32
analogue controls. It is not a cache of something else; it is the only copy. It
already publishes **one** control — the currently displayed OLED line, in
`/tmp/lcxl3-oled-ghost.txt` (340 bytes, the sent-side truth).
That is the whole design constraint: state has a single legitimate owner, and it
is not the viewer.
## Three rules for the wire
### R1 — one owner per fact, two channels because two physics
State is a **level**; events are **edges**. Do not derive state in a consumer
(it cannot), and do not tunnel events through the state channel (coalescing
would eat exactly the timing an event is for). Two channels, each doing one job:
| channel | owner | shape | consumed |
|---|---|---|---|
| state | `lcxl3-driver` | 32 values, last-value-wins | on the frame tick |
| events | the wire (`aseqdump` on the driver's translated port) | stream, edge-timed | as they arrive |
| orbit activity | the editor, **already published** | `eval-events.jsonl` | on the frame tick |
### R2 — last-value-wins state in one small file, written atomically
The driver already coalesces its OLED writes at ~10 Hz; the state publish rides
that same tick, no new timer. Write a temp file and `os.replace()` it: readers
never see a torn file, there is no lock, no broker, no connection state, and
`cat` is the debugger.
Carry a monotonic `seq`. A consumer that sees an unchanged `seq` does **zero**
parsing and **zero** repainting. At rest the state channel therefore costs one
`stat()` per consumer frame — which is the "pay once" property being asked for.
*Why not a socket or a pub/sub broker:* the payload is ~32 integers, idempotent,
with no history worth queueing. A socket adds a connection, backpressure, and a
dead-consumer failure mode, in exchange for nothing. That is a screw with no nut.
The precedent is already in the tree and it is the driver's own words about the
editor's files: *"The driver does not need a new channel, a socket or a plugin:
it needs to READ."*
### R3 — paint only what changed
`midiviz` already runs two frame rates (≈30 fps while events arrive, 5 fps two
seconds after the last one) with a precomputed colour LUT, so "a frame allocates
essentially nothing". The state layer must not undo that. It is **static at
rest**: render it once into a pixmap, blit, and repaint only the cells whose
value actually moved — `seq` says whether anything did, the diff says which.
Adding state should therefore cost approximately nothing when nothing is
happening, which is the opposite of adding a second animated layer.
## What NOT to build
Each of `midimon`, `midiviz` and `surface.py` spawns its **own** `aseqdump`, so
running two of them means ALSA delivering the same bytes twice and two processes
parsing them. The tempting fix is a fan-out daemon.
Don't. It adds a process, a failure mode and a latency hop to save a
subscription that costs microseconds, and these tools are not run together in
practice. Parsing is already shared at the source — one `parse_line` / `enrich`
for the CLI, the window and the web stream — which is the part that mattered.
And if fan-out ever *does* matter, the driver already publishes a stable virtual
port: that is the fan-out point, already built.
## First slice: the DJFs, and the detent is the point
The three family filters (`gF1` drums, `gF2` bass, `gF3` leads) live on C1/C2/C3
and are the controls PLN actually named. Their meaningful states are LOW / ZERO /
HIGH — and **zero is the centre detent, value 64, not 0**. A 0–127 bar does not
say that; three zones with the detent drawn do, and that matches the mental model
the hardware trains.
Scope: three strips fed from the state file. An evening, not a project, and it
answers the complaint on its own.
## Then: the Pulsar question, which is cheaper than it looks
`~/.cache/parvagues/eval-events.jsonl` **already exists and is being written** —
one JSON object per ctrl+enter:
```json
{"t":1790088188.93,"path":"live/midi/nova/breaks/bain_electrique.tidal",
"d":["d1","d2","d3","d4","d5","d6","d7","d8","d9","d10","d11"]}
```
Alongside it, `~/.cache/parvagues/current-track`. The driver already reads both,
after PLN reported the same bug three ways ("i see still ROSE_ROUGE in title").
So "read events as the tidal pulsar" does not need a new producer. Tailing that
file tells a viewer which orbits are live in the loaded track — which is what
turns the picture into an answer for the real stage question: *I turned the knob
and nothing happened — is the pattern silent, or is the knob not wired?* Surface
state answers the second half, orbit activity the first.
It is not beat-level — per-event note activity would need an OSC feed from
SuperDirt, and that is the only genuinely new component in any of this.
## Order
1. theme / scale / tray menu (in flight) — the sunlight problem, tonight.
2. the DJF three-zone strip off a driver-published state file.
3. the full 48-cell state layer, dirty-cell repaint.
4. `eval-events.jsonl` as an orbit-activity layer.
5. only if still wanted: a SuperDirt OSC feed for beat-level activity.
Steps 2–5 are after Thursday. Nothing above changes the driver's audio path.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment