feat(gig-log): log WHICH TRACK is loaded — the key that decodes the whole MIDI stream
PLN recorded an afternoon of OPAL takes, then asked for "a master split per track loaded time". The answer should have been trivial. It was not: nothing in the rig recorded WHEN each track was loaded. The gig log had 78222 samples of thermals, xruns, gear and 7429 coalesced MIDI CC events for today alone — and no way to say what any of it was. A CC 41 is meaningless on its own, because the LCXL map is PER TRACK: on one track index 41 is a crush bus, on another it is a break gate. The performance data was all there and none of it was resolvable. Meanwhile the exact ground truth was sitting in a 33-byte file the whole time. Pulsar has published `~/.cache/parvagues/current-track` since #84 (2576e77) for the LED watcher to follow. Nobody was reading it into the log. WHY THIS MATTERS MORE THAN THE SPLIT IT WAS ASKED FOR Splitting a livecoded set by ear is genuinely hard, and we have the scars: orbit roles stay stable across tracks so level/presence detection fails, and every dN is a 4-cycle crossfade that bleeds the incoming track into the outgoing one's tail — the existing Bandcamp splits carry that bleed. A `k:"track"` line replaces all of that inference with a timestamp. Boundaries stop being clever and become read. And it retro-actively upgrades the CC stream from telemetry to an annotated performance score: track + CC + time resolves, through the per-track LCXL map we already generate, to "d4's DJ filter opened at 18:42:13". That is the input for telling practice apart from performance (hands-off vs hands-on density), for finding the moment worth clipping, and eventually for the emotion/timeline bundle. IMPLEMENTATION — deliberately the boring version - `_read_track()` never raises: absent file (publisher not up yet) or a short read both degrade to None. A missing track must degrade the log, never stop it. - Written ONLY on change, so a 3-hour session costs a handful of lines instead of 10800 repeats of one string. The header carries the initial value, so a session that never switches still has one authoritative statement of what was loaded. - Polled, not watched — for the same reason the LED watcher polls. One stat+read of 33 bytes per second is unmeasurable; an inotify watch adds a dependency and a whole class of "the watch silently died", which is the exact failure mode this data exists to prevent. - `load()` folds track records into `marks` so they render in the existing timeline for free, keeping `k:"track"` so a splitter can tell an automatic boundary from a human annotation. VALIDATED - `selftest` PASS; sampler + both readers still 0.66% of one core over 6s, well under the 3% observer budget (the observer must not perturb — we already learned that lesson when probe-chain's own capture streams caused the xruns). - END-TO-END, not just unit: ran the real recorder against a temp track file, moved it twice, and read the log back. Header carried the initial track; both changes appeared exactly once with their `from`, each within the 1s poll. Green tests on a pure function would not have proven the value reaches the file.
Showing