Commit b3dddedb by PLN (Algolia)

docs(tasks): archive #75/#11/#76/#68/#69 — and a morning briefing to play from

Five tasks closed overnight, written up as long-form entries rather than one-liners
because these are the documentary trail: each carries the original symptom in PLN's
own words, the mechanism, the numbers, and the wrong turns.

The through-line across all five, worth naming: EVERY ONE of them found a second,
worse bug than the one it set out to fix, and in four cases the second bug was found
by LOOKING AT THE OUTPUT rather than by reasoning about the code.
  * gig-log's report exposed its own xrun baseline as nonsense (72096 xruns on an
    idle machine) — twice, before the third rule held.
  * rendering the LED board exposed the half of the panic bug that lived in
    parse_track, after the half in control_colour was already fixed.
  * the per-track probe PLAN exposed that lens.py could not see gF1/gF2/gF3 at all.
  * wiring a status label into the tray exposed that it opened the ARCHIVE Ardour
    session, could never have launched Ardour anyway, and reported it running when
    it was not.
A tool that prints what it believes is a tool that can be caught lying.

MORNING.md is the other deliverable: PLN opens the laptop with a coffee to "just
play", so the state of the rig has to be readable in 30 seconds — what changed on
the surface, the one 5-minute thing worth doing (the ghost report), and what is
waiting on his ears rather than on work.
parent 8bbcfd98
# ☕ Morning, 2026-07-29 — OPAL Day-6
**Everything is running. Nothing needs starting. Go play.**
The night's work is on branch `claude/preload-eager-banks`, 5 commits, **435 tests green**.
---
## Just play — the three things to know
**1. Your run-through is being recorded (numerically) already.**
`gig-log` has been sampling at exactly 1.000 Hz since 00:57 — temps, CPU freq, throttle
counts, xruns, per-gear %CPU + RSS, and every LCXL control move. It's a systemd unit, it
starts with your session, and it costs **0.4% of one core**. Wall-clock stamped, so it
lines up with your Ardour take.
```bash
tools/gig-log.py mark "gimme acid drop" # annotate mid-set, any time
tools/gig-log.py report # read it back afterwards
```
When you finish, run `report` and we'll review the run together.
**2. The LCXL looks different — on purpose.** Both things you spotted are fixed:
| | before | now |
|---|---|---|
| DJ filters (row C) | wash of oranges, 7 steps | **red** = cutting highs · **bright yellow** = bypass (centre) · **green** = cutting lows |
| knobs (rows A/B) | dim / not-dim only | your six-step ramp, wired in at last: dim red → red → dim amber → amber → dim green → green |
| mutes 73/74/75 | `R R G` 🤨 | all three identical |
| panic armed (`^93`) | invisible | **all four chord buttons flash red** |
Rows A/B will be uniform dim red at rest — that's "nothing brought in yet", and things
climb toward green as you bring them up.
**3. Right-click the tray → `Gear ▸ 3/6 up`.** It now says what's *already running*
(● running / ○ stopped / ✗ missing) instead of only offering to launch.
---
## One thing worth 5 minutes before you play
**Your set leaves ghosts at 8 of 12 transitions.** That's the crimewave synth you heard
under bombe_dj — `dN` replaces orbit N and says nothing about the others, so any orbit the
outgoing track declares and the incoming one doesn't just keeps playing.
```bash
tools/orphan-orbits.py # the whole set, transition by transition
tools/orphan-orbits.py --matrix # which orderings are cheap
```
- Under pressure: **`hush` between tracks.** Safe, costs the tail.
- **Perfect <3 declares all 12 orbits** → it's a free *reset point*. Anything can follow
anything into it. Useful ordering lever for #12.
- Worst offenders: after REVOLUTION (4 orphans), after Gimme Acid (3).
Set list is now data: `armada/setlist_opal2026.txt`. Reorder it and re-run to re-answer.
---
## Waiting on your ears / hands (not on work)
1. **`pv-at --track` — run it with sound up.** It probes every control on a track and says
MOVED / NO_IMPACT / INCONCLUSIVE. Built and unit-tested; the audio half has never run
because the house was quiet. Start with `python3 tools/at --track live/midi/nova/acid/gimme_acid.tidal --seconds 4` (~8 min, 22 controls).
Any **NO_IMPACT is a real find** — a mapped control that does nothing.
2. **#46 the surface remap** — needs you to re-learn Ardour's 12 gains by hand, and to move
`piment_bresilien.tidal:77`'s crush off `^14`. Verify fader→orbit *by behaviour*; the
saved session disagrees with the live mixer (#62).
3. **#8 thermals under load** — needs monitors + a heavy track for 10 min. gig-log now
captures exactly what's needed. (Baseline so far tonight: **0 xruns, 0 throttle events.**)
4. **#70** you_my_sunshine d5/d11 cut-group clash, **#12** set order, **#13** transitions.
---
## Found while working, worth knowing
- The tray was wired to open **"Tidal Multi"** — the *archive* session — not "Tidal Live".
Fixed. It also could never have launched Ardour at all (looked for `ardour8`, you have
`ardour9`), and its running-check matched any shell that merely mentioned the word
"ardour", so it could report Ardour up while it was down.
- `lens.py` couldn't see `gF1/gF2/gF3` **at all** — they live in `BootTidal.hs`, not in the
tracks — so the acceptance test was blind to the three DJ filters and every mute. It now
parses BootTidal. gimme_acid went 18 → 22 controls.
- Two of your three mutes were being measured with the wrong lens (`mask "f*16"` read as a
density gate rather than a mute), so two working mutes would have reported as dead.
- `perf._read(None)` crashed instead of returning a default — the Bridge shares that path.
Full narrative is in the commit messages (they're written to be mined for the blog).
**Nothing I touched needs a reload or a restart. `check-boot.sh` passes. Have fun. 🤘**
......@@ -674,3 +674,161 @@ yield events against an EMPTY control map.
call sites first, delete last. Reversed, it is a 179-track mute-bomb.
**Deps.** Unblocked #46. Spawned #73.
---
## #75 — gig-log: a session recorder, so a run-through can be REVIEWED not just felt
**Description.** After a set, "did it glitch?", "did it get hot?" and "which controls did I
actually use?" were answered from memory. PLN asked for logging in place before his morning
run-through, which he records in Ardour: *"we def gotta ensure tomorrow morning when i start
you have logging in place so we can then review the perf and validate."*
**Done** (`4c57362`). `tools/gig-log.py record | mark | report | status | install | selftest`,
running as an enabled `gig-log.service`. 1 Hz JSONL: temps, fan, freq, core+package throttle
deltas, per-core cpu%, per-gear %cpu + RSS (scsynth/sclang/ArdourGUI/whole pulsar group),
PipeWire xruns per node, and every LCXL move. Wall-clock stamped so it aligns with the Ardour
take. `--report` renders sparklines, an xrun timeline, a gear table and a per-CC surface table
with first/last touch. 71 tests.
**Learnings.**
- **Measured, not asserted: 0.40% of one core** (270 pw-top lines parsed in 10 s), 0.79% as
the live unit. My first selftest said 0.66% and was *flattering itself* — it never started
the reader threads, so it measured a sampler with nothing to parse. Same mistake as the
first `--bench` run in the LED work. Re-measured lower AND honest.
- **The logger must not be a firehose.** CC/pitchbend coalesced to one line per control per
second with count + first/last/min/max — the #71/#72 fix applied to ourselves. Notes are
never coalesced: a CC is a state, a note is an event.
- **Three wrong xrun baselines before the right one**, each caught by reading the report:
first-sight → 72096 xruns on a 25 s *idle* session (pw-top prints a zero-filled table
before the profiler has data); second-sight → silently wrong when TWO zero tables landed;
finally **max over each node's first 5 sightings**, which needs no timing assumption
because ERR is monotonic. A rule that is right *most* of the time is the worst kind for a
gig log, because the one bad reading looks exactly like a disaster.
- **Ardour accumulates ~6 xruns/min with nothing playing** (67072 → 67123 → 67134 across
captures minutes apart). Only the session delta may ever be quoted.
- Verified in production: **exactly 1.000 Hz, p99 gap 1.001 s, no drift** over 5654 samples.
- Fixed in the shared reader: `perf._read(None)` raised TypeError instead of returning the
default, so a machine with no coretemp/dell_smm hwmon **crashed** the thermal read — the
Bridge shares that path.
- `/proc/<pid>/stat` must be split after the LAST `)`: pulsar renderer comms look like
`(pulsar) --type=renderer`, so splitting from the left silently reports the wrong column.
**Deps.** Unblocks the measurement half of #8 (thermals under load), #56 (Pulsar starving
audio) and #14 (rehearsal review). Feeds #44.
---
## #11 — LCXL colour, settled: red lows / bright-amber centre / green highs, and the ramp finally wired
**Description.** Two taste calls PLN settled by eye on the hardware: sunset the dim-amber
steps on the DJ filters (*"just have red in lows green in highs for clarity"*, *"i agree on
clarity > resolution"*), and make the knobs show more than on/off.
**Done** (`0b23f3d`). `filter_colour` reduced from seven steps to three palette corners —
red / bright amber at the 61-67 detent / green. `value_ramp` wired into `control_colour` for
knobs. Module header rewritten to describe the convention that now exists. +24 tests.
**Learnings.**
- *"wait 'brightness value 6 steps ramp'? I See only two states, dim and not dim"* — he was
right. `value_ramp` was written, unit-tested, documented in the header, and **never
called**; `control_colour` had three outcomes, so 30% and 90% were the same colour. The
old knob rule had **no test at all**, which is exactly how a never-called ramp shipped.
- The old filter ramp spent four of seven steps on shades either side of centre, so the row
read as a wash of oranges and the one thing you need — *which way is it cutting* — was the
hardest bit to see. Three unambiguous states beat seven nobody can resolve on a dark stage.
- **The trade, stated so it cannot regress silently:** the ramp spends all three hues on
VALUE, so hue no longer carries ROLE on rows A/B/C. Right way round — role is fixed by
POSITION, a knob's setting has no other channel. Keeping "untouched" as a colour collided
(dim green would mean both "rhythm, untouched" and "value ~3/4"); it moves to the HUD.
**Deps.** Closes the #11 taste call. #49/#51 (colour-by-function, legend) still open.
---
## #76 — panic red must track ^93, not chord membership
**Description.** PLN: *"why are [mutes] 1/2/3 resp red red green? these 3 buttons have same
roles, why diff colours?"*
**Done** (`0b23f3d`). `panic` threaded in from the model (`values[93]`); chord members paint
by role when clear; arming flashes red on all four as a global overlay applied last.
**Learnings.**
- **The panic chord is an OVERLOAD, not a dedicated function.** Hold 73+74+91+92 → SC
edge-detects and flips persistent `^93`. Those buttons individually are gMute1/gMute2. The
panic identity **does not exist at rest**, so painting it at rest is simply wrong.
- **The bug was in TWO places**, and the second was only found by *rendering the board*
after fixing the first: `control_colour` tested chord membership before role, AND
`parse_track` force-bound all four members to role "fx" via setdefault. A chord is not a
binding. Verify by rendering, not by reasoning.
- `^93` owns no LED (CC93 is outside row F's 89-92), so those four buttons are the only
place the armed state can live — and it is the highest-value LED in the rig, because it
answers *"why is there no sound?"*. Hence overlay-over-dark, not overlay-over-role-only.
- The watch loop rebuilds the whole frame on a `^93` flip: one event changes four LEDs, so
it is the one event a single-index paint cannot serve.
**Deps.** #34 (panic live-verify) still wants PLN's hands on the four buttons.
---
## #68 — orphan orbits: name the ghosts before the stage does
**Description.** PLN by ear: loaded vague_de_crime, played bombe_dj, *"heard diam's voice,
now still hearing the synth from crimewave... i hear it regardless of gains, but filters work
on them"*.
**Done** (`3ab9ae0`). `armada/setlist_opal2026.txt` (the set as data, resolved from
backlog codenames by locating each file) + `tools/orphan-orbits.py`
(chain / `--matrix` / `--pair` / `--silence` / `--json`) + 39 tests.
**Learnings.**
- `dN` replaces orbit N and says **nothing** about the others. **Both of PLN's symptoms are
the proof and are diagnostic in general**: "regardless of gains" because the new track's
gain lines address only its own orbits; "filters work on them" because gF3 is the global
CC51 filter applied *inside* the ghost's own still-running pattern.
- **8 of 12 OPAL transitions leave ghosts, 16 orphaned orbits.** The ear-report reproduces
exactly: vague_de_crime → bombe_dj leaves {6, 10}, and d6 IS the crimewave synth.
- **The find worth more than the report: Perfect <3 declares ALL TWELVE orbits, so its
matrix column is entirely clean — a free RESET POINT.** Put it after the messiest stretch
and ghosts stop propagating. A real ordering lever for #12.
- **Cross-validated**: the parser's orbit sets match the ten hand-measured on 2026-07-28.
Tool and ear agree.
- **Stopped short of automating the fix, deliberately.** `eval-file` calls
`atom.workspace.open(..., activatePane: true)` — it opens AND activates, so it would yank
the editor away from the track being played mid-transition. Worse than the ghost.
- `silence`, never `# gain 0`: gain 0 leaves the pattern scheduled, burning a voice, and a
later global gain change revives it.
**Deps.** Feeds #12 (order) and #13 (transitions). Fix choice needs PLN.
---
## #69 — the tray now says what gear is ALREADY UP
**Description.** PLN: *"i rightclick the perf indicator and see no gear status?"*
**Done** (`8bbcfd9`). A `Gear ▸ 3/6 up` submenu labelled ● running / ○ stopped / ✗ missing,
refreshed on menu-open only. `is_running` rewritten forkless. +11 tests.
**Learnings.**
- The state existed all along (`launchers.is_running`); `_build_menu` ran once at startup
and read only `available`. **The safe action ("it's already up") looked identical to the
dangerous one** — two SuperColliders is a zombie port 6010 and a silent rig.
- **THREE worse bugs surfaced while wiring it up, each found by checking the output:**
1. `ARDOUR_SESSION` pointed at **"Tidal Multi"**, the ARCHIVE, not "Tidal Live". The tray's
Ardour button opened the wrong session — the same mixup that produced a confidently
wrong fader report on 2026-07-28, one layer down, 6 days before OPAL.
2. Candidates were `ardour8/7/6`; the installed binary is `ardour9` (`ardour-9.2.0`). The
entry reported "unavailable" while Ardour was running on the same machine.
3. `pgrep -f ardour` matched the `tidal-ardour-autoroute.sh` helper **and any shell whose
command line mentioned the word**. It could report Ardour UP while it was DOWN — worse
than reporting nothing, because it is the state you act on. Now matches the executable
basename, with full-line matching kept only for interpreted tools.
- **No forks:** one `pgrep` per launcher became a single shared /proc scan — 6 items in
10.7 ms, and `snapshot()` is called by the web Bridge's poll too. Same lesson as the LED
daemon's per-event fork.
- Glyphs not colour: a tray menu inherits the desktop palette, and this gets read in a dark
room seconds before playing.
**Deps.** Unblocks #25 (the Bridge as single cockpit).
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