Commit 13a053c6 by PLN (Algolia)

gig: TODO_GIG is a card you can read at 18:00, not a week of notes

1101 lines of diagnosis had accumulated on top of each other, oldest first,
so the only live content — tonight's clock and the four things left to do —
sat at the very bottom under forty headings dated Sunday. A checklist you
have to scroll to is not a checklist.

Rewritten as one screen: the clock, five numbered actions in the order they
happen, four symptom-to-fix recoveries, four things never to run tonight,
and the measured rig state. Short lines, plain words, no jargon that needs
a lookup at showtime.

The week's notes are not lost and not duplicated — the file's own footer
carries `git show 50adcf43:TODO_GIG.md`, verified to restore it byte-exact.
parent e90d275b
# TODO_GIG — Thu 24, 2h set (written Sun 21 night) # TONIGHT — ParVagues, 2 hours
Set material: shortlist pending from PLN. Rehearsal Tue 22. Rig: XPS24 (corpo box — pause corporate agents on gig night; XPS22 stays home). **Thu 24 Sept. You play 18:00 → 20:00. 17 tracks, ~120 min. It fits.**
## Tonight — setup (Sun 21)
- [ ] **OLED H1 probe + fix** — PROBE DONE 2026-09-21 23:00: **H1-AFTER confirmed** (bring-up
after write renders on 0x35 and 0x36; bare writes don't — answers JSON:
`/tmp/claude-1000/-home-pln-Work-Sound-Tidal/0f7ff2b8-ccb4-40d8-b86c-5741f929f419/scratchpad/display-probe-results.json`).
H2 rate ceiling unresolved (aborted — test at Tue with fresh eyes). Driver-fix builder running:
bring-up-after-write + ~10 Hz coalesce + persistent at-rest DJF.
Still to do: driver restart + sanity sweep (ghost ~15 Hz, glass legible per-move).
- [ ] **Rig perf** — governor → performance + `max_perf_pct 100` (sudo, PLN runs the sudo line:
`echo 100 | sudo tee /sys/devices/system/cpu/intel_pstate/max_perf_pct`). BUILDER FINDING:
perf.sh governor glob covers ALL cpus, but the `--optimize`/standard path never sets
max_perf_pct — a leftover cap from a prior `--cool`/`--silent` (55%!) silently survives a
standard perf pass. The sudo line is mandatory, not optional. This is the preflight FAIL
behind the 270 xruns/h signal.
- [ ] **s.latency 0.2 → 0.1** — EDITED in `start_and_midi.scd:430` but **NOT LIVE**
(verified Tue 22: sclang running since Sun 20 17:57, file edited Sun 21 22:48 — the process
still holds 0.2). Needs a `systemctl --user restart parvagues-sc`, which interrupts play, so
do it at the START of tonight's rehearsal. AC after 10 min of playing:
`journalctl --user -u parvagues-sc -b | grep -cE '\blate [0-9]'` still 0 — revert to 0.2 on
any 'late'. (The 0-late reading taken Tue 22 measured the OLD 0.2 config and proves nothing.)
- [ ] **Quantum 512 test** — `pw-metadata -n settings 0 set clock.force-quantum 512`, listen to
Keys/Mic monitoring vs 1024. Keep-or-revert decision recorded here: ____
- [ ] **BT hardening** — `rfkill block bluetooth` behind a gig-up flag (the wireplumber-churn
killer; today only gated by luck). AC: gig-up --bt-off blocks BT, default unchanged.
- [ ] **Set validation** — DONE OVERNIGHT (full report: `/tmp/overnight-set-validation.md`):
**7/9 candidates CAN PLAY** with the standard #55 boot seed. Caught 3 nights out:
- `cyber_hump` had a stray `)vi` (line 384-era) — **every orbit dead**. FIXED + committed,
re-verified: all orbits emit with seed.
- `techno_orage` **d2 (line 158) silent even WITH seed** — real fault, needs PLN's ear/eye.
- Preload gaps: `bass2` (ete_a_mauerpark) + `humps` (cyber_hump) not warmed → lazy-load
mid-set. Morning: regenerate preload via `setlist_samples.py --emit-sc` flow WITH PLN
(load-bearing boot file — measure before mechanism), or accept 2 first-hit reads.
- [ ] **Hygiene** — Pulsar `~/.pulsar/config.cson`: autocomplete-plus delay ≥300 ms,
`showErrorNotifications: false`, highlight RAF fps 30 → 25 (Tidal emits 25 Hz). Neutralize
stale `tools/pulsar-parvagues-hud` copy (v0.1.0 vs installed v0.2.0 — pre-gig backwards-fix trap).
## Tue — rehearsal (with Ardour open, full set)
- [ ] **Master fader at −30.2 dB** — MEASURED Tue 22 15:1x with Ardour running
(`tools/check-mix.py`): the 12 orbit faders are FINE (+0.0 to +6.0 dB, none muted, all to
master) — the overnight "10/12 at −inf" claim does NOT hold for the current saved session.
The single failure is **Master −30.2 dB**. check-mix reads the last SAVED state and
Ardour OSC is off, so the LIVE fader is unverifiable from outside: PLN must look at the
Master fader, decide the reference level, and SAVE so it survives the next launch.
- [ ] **Enable Ardour OSC** (use-osc=0 today) → preflight verifies LIVE fader values; closes the
−inf blind spot for good.
- [ ] **Delete 8 dead sends** on Tidal 01/09/11 (six + one + one, unconnected leftovers).
- [ ] **xrun re-measure clean** after the perf fix (old 270/h window may be contaminated).
- [ ] **Full-set run** on chosen quantum; late-count check (AC above); highlight-stats pass.
- [ ] **Gig-day dry run of `gig-preflight.py`** to green-minus-accepted-warns (fold links 0/28,
docker off, pulsar launch, LCXL mk2-dialect-on-mk3 accept-or-fix).
- [ ] **Decide Master chain under pressure** — LSP Limiter is the only PA protection; viz procs
optional if pw-top shows pressure.
## Post-gig backlog (do NOT touch before Thu)
quantum re-derive vs 50-alsa-config · sample-watcher as gated service · boot-time trimming ·
DJF readout into HUD via Ardour OSC · record-arm automation at session open · batch highlight
add-bursts · re-encode 120MB scene twins (crf 28) · event-driven reconcile (pw-link -m) ·
bitmap-rendered DJF needle · Pulsar upstream #229 / tidal.nvim evaluation.
## Tue 22 — state (2 DAYS OUT, gig Thu 24)
Shipped + live today: OLED bring-up-after-write (9507ad6), all 32 knobs on our overlay
(9a4918a), stale-copy tripwire (8f00e29), cyber_hump revived (0f99fba), encoder end-stop
slam (8e7b713), lookahead + BT flag (aa41686).
Open, highest leverage first:
- [ ] **YOU: lift the CPU cap** — `max_perf_pct` measured at **55** right now (governor IS
`performance`, so perf.sh ran; the pstate cap survives it). Until this is 100 every audio
measurement (xruns, quantum, headroom) is untrustworthy:
`echo 100 | sudo tee /sys/devices/system/cpu/intel_pstate/max_perf_pct`
- [ ] **YOU: pick + order the 2h arc** — Set Builder: http://192.168.1.11:8899/setbuilder.html
(status page: http://192.168.1.11:8899/status/). Export the paths back here and the preload
plan + a final silent-eval pass follow.
- [ ] **SC restart** to load 0.1 latency (see above) — at the start of the rehearsal.
- [ ] **OLED stale-value fix** — root-caused Tue 22 by code reading, fix in flight:
(1) `tick_overlay`'s 0.9 s re-arm REPLAYED cached strings, so a stale readout was reprinted
forever; (2) `oled_touch`'s 20 Hz front door DROPPED events without parking any pending
value; (3) `run()`'s "pinned at an end stop" `continue` skips the display entirely at 127.
Together: a hard DJF swing froze the readout (PLN saw "HPF 5.6kHz" ≈ value 108 while the
filter kept moving; 127 = 8.0kHz, and each count moves the string ~0.13 kHz so the readout
is NOT quantized — it was genuinely stuck). Fix = recompute on re-arm (self-heals within
0.9 s), park instead of drop, flush the freshest value. Momentary `family_mute` lines still
replay (mutes_held() is transient).
- [ ] **End-stop slam is UNMEASURED** — shipped with a `|d| >= 63` trigger, but a read-only
75 s probe captured **0 CC messages** (PLN was away), so whether the firmware ever emits a
saturated ±63 delta is unknown. Likely dead code. Re-run
`scratchpad/delta_probe.py` while PLN sweeps, read the delta histogram, THEN set the
threshold from reality. Builder also flagged 123 → 127 overshoot at the `>=115` band.
- [ ] **CANDIDATE ARC, not locked** — `armada/setlist_thu24.txt`, 10 tracks, 120/120 min,
in play order (commit 2fbb7d3), exported from the Set Builder Tue 22. PLN's words Tue 22
evening: "this set is not locked, just a potential one." All 10 pass
`silent-eval --seeded`, including `something_about_drums` (never validated before); the
arc needs 46 banks, all 46 on disk. Rehearsal decides what stays.
- [x] **Preload covers the set** — `preload.scd` regenerated as a SUPERSET: 125 banks
(39-track 12-month list ∪ the 10 Thursday tracks), adding `bass2` for ete_a_mauerpark.
Warms at the next parvagues-sc boot. Backup in scratchpad; file is gitignored by design.
- [x] **Gig-night landmine defused** (c52de40) — `gig-up` runs `check-preload.sh --fix`
whenever the check fails, and the old gate scored ANY superset as STALE (`comm -3` over
name+count pairs), so a plain `gig-up` at the venue would have regenerated from the
39-track list and silently dropped `bass2` again. Now: missing = fault, extra =
information, counts join on shared banks, `--fix` emits the union and cannot shrink.
Second hole found and closed: under `en_US.UTF-8` a whole-line sort orders `bass2 5`
before `bass 4` while `join` compares field 1, so join dropped a pair (123 of 124) and
`2>/dev/null` ate the warning — a count check with a hole reports ok. `LC_ALL=C` +
`sort -k1,1` everywhere. Verified on the live plan: `ok — 125 banks`, **exit 0**, so
gig-up no longer reaches `--fix`. Full suite 335 passed (1 pre-existing fold-orbits
ratchet failure, unrelated).
- [x] **Catalog measured** — `docs/2026-09-22-catalog-playability.md` (080b7f7): 703/703
evaluated, **84.9% playable**, 12.1% compile-fail (diffuse helper/API drift: `not in
scope: g`, `jchord`, `Pattern String` type mismatches), 3.0% silent-orbit.
- [x] **QjackCtl in the tray** (fa0ec29) — the row existed but bare; `libjack.so.0`
resolves to jackd2's, so it showed an empty jackd2 graph. Now wrapped in `pw-jack`.
Bites on the next `systemctl --user restart perf-tray`.
- [x] **Column remap finished on the tracks in play** (d8effb6) — the #94 remap was run
against a setlist, so conformance was never a property of a track, only of having been
listed; and Tue's desk edits pushed listed tracks back out (swapping jeudrill's d9/d10
bodies moved the orbits, not their knobs). 24 moves over six tracks. **WHERE YOUR HANDS
HAVE TO GO NOW:**
| track | orbit | what | was | now |
|---|---|---|---|---|
| jeudrill | d4 | crushbus | C5 | **B4** |
| jeudrill | d4 | octersubsub | B5 | **C4** |
| jeudrill | d5 | mask gate | BT6 | **BT5** |
| jeudrill | d7 | slice | A7 | **B7** |
| jeudrill | d7 | speed −0.25 | BT7 | **BL7** |
| jeudrill | d7 | struct | BL7 | **BT7** |
| rose_rouge | d4 | crushbus | C4 | **B4** |
| rose_rouge | d4 | octer/octersub | B4 | **C4** |
| rose_rouge | d5 | note pattern | C6 | **B6** |
| liquid_nite | d3 | legato | B4 | **B3** |
| liquid_nite | d5 | crushbus | C6 | **B5** |
| liquid_nite | d7 | crushbus | C7 | **B7** |
| liquid_nite | d8 | loopAt COMEON | C8 | **B8** |
| cyber_hump | d4 | crushbus | C4 | **B4** |
| siberie_samuel | d8 | gate | BT4 | **BT8** |
techno_orage took 9 more. Verified: re-plan 0 moves on all six; silent-eval --seeded OK
on five (techno_orage still fails on the same pre-existing d2); pvlint 0 errors; and
zero writes landed in 13-16 / 49-51 / 77-84, the Ardour-learned and gF ranges.
- [ ] **techno_orage: ^58 drives d5 AND d6** (pvlint PV008) — the overflow the migrator
annotated rather than commented out, because it is a `$`-step and commenting it would
break the block's arity. That button really does fire across two columns until gSel
folds it. Played twice this week, so it matters.
- [ ] **Corpus-wide remap: 154 files want 709 moves, ZERO clashes** — not tonight. It is a
mass rewrite of `live/` and needs PLN's explicit go-ahead; the sandbox refused it, which
was the right instinct two days out. Run after the gig:
`find live -name '*.tidal' -print0 | xargs -0 python3.12 tools/migrate-columns.py --apply`
- [ ] **90 files carry 114 duplicate FIXME lines** — the migrator used to stack a
byte-identical note on every run (cafe_glace had the same paragraph three times). Fixed
at the source in d8effb6 so it stops growing; the existing 114 are cosmetic, post-gig.
- [ ] **"The set" still resolves to Opal 2026** — `tools/migrate-columns.py --plan` with no
args, via `tools/setlist.py` → `backlog.md`, walks bombe_dj / wap / do_it_right /
take_5_drops. The Thursday arc lives only in `armada/setlist_thu24.txt`. Pass paths
explicitly, or give backlog.md a Thursday section, before trusting any default.
## Tue 22 — evening. The set is settled and the surface is coherent.
- [ ] **YOU, FIRST THING: seven orbit faders are at −inf in the SAVED Ardour session.**
`tools/check-mix.py` reads Tidal 04, 05, 06, 07, 08, 10, 12 all at −inf, Master −0.5 dB.
`tools/fader-baseline.py --check` calls it: 8 faders drifted, and the baseline wants
04 −0.4 / 05 0.6 / 06 0.6 / 07 0.6 / 08 1.5 / 10 5.6 / 12 6.0 dB. From a fresh Ardour
launch, **seven of twelve orbits are silent**, and it looks exactly like Tidal emitting
nothing because Ardour's meters are post-fader. This is not new — the archived 6 Sept
notes have PLN's own words on it ("tidal 12 -inf is random i might turn on/off any
fader ... i save random") — and the fix is already wired: `gig-up.sh` runs
`fader-baseline.py --restore` in the Ardour-closed window. So either **gig-up at the
venue**, or close Ardour and run `tools/fader-baseline.py --restore` yourself. It needs
Ardour CLOSED; while it is open the tool refuses.
This now matters MORE than it did this morning: the orbit compaction moved melodies onto
d5 and d6, which are two of the seven.
- [x] **THE SET: 17 tracks, AI ENGINEER** (fe4441d) — `armada/setlist_thu24.txt`, in play
order, three movements (START / NUJAZZ PARADIZE / FINALE COMEDOWN), ~9 min each. PLN's
own arc, and the shape moved with his ear: cyber_hump, wap and bombe_dj are out ("too
sample heavy its a nujazz/chill dnb set"). **All 17 pass `silent-eval --seeded`**, ten of
them never validated for this gig before. pvlint over the set: **0 errors**.
- [ ] **YOU: `ete_a_mauerpark` — confirm the d9 effects** (his note).
- [ ] **YOU: `sept1` — fix to the new controller** (his note).
- [ ] **YOU: A/B two crush buses** (ff084fa) — `love_first` d12 and `cafe_bouillant` d11
were both writing `crushbus 41` alongside d4, so the two parts traded crush amounts
every cycle. Moved to 121 and 111, the numbering the corpus already uses. Each part
should now answer its own knob; worth one listen since it is an audio-graph edit.
- [x] **Preload covers the new set** — regenerated as a union: **125 → 132 banks**, nothing
lost, exactly the 7 the new tracks needed gained (`hh27`, `jungle_bass`, `movie_cat`,
`nujazz_bassp120`, `nujazz_fx120`, `nujazz_guitar120`, `rampleS57`). Both gates exit 0.
Mode 0600 preserved, backup in the session scratchpad. **Warms at the next
`systemctl --user restart parvagues-sc`** (~15 s of silence, safe when not playing).
- [x] **Corpus-wide column remap DONE** (3827b62) — 685 moves in 148 files, 0 clashes.
Button conformance **78.9% → 93.3%**. Zero CC refs landed in the Ardour-learned or gF
ranges; every bus slot byte-identical.
- [x] **Orbit compaction DONE** (f6be520) — 150 moves in 133 files, band {5,6,7} derived
from the family maps so a move is inaudible; d4 (bass) and d8 (percs) never touched.
7 orbits refused for carrying an explicit `# orbit N`.
- [x] **THIRD PASS: the controls follow the orbits** (ca21d6b) — compaction renames a `dN`
but does not move that orbit's knobs, so 202 controls in 84 files were left on the column
of an orbit that had moved away. The two tools are one job in two passes. Found by
accident: `ete_a_mauerpark` reported "0 move" right after the compaction and 4 the moment
something else made me re-plan it — **a conformance check run before the step that breaks
conformance proves nothing**. Re-plan now 0 across all 703; third full sweep, zero verdict
changes; the 17-track set re-validated after, all OK, pvlint 0 errors.
- [x] **`ete_a_mauerpark` d9 — PLN's "[confirm d9 effects]" answered.** d9 gets exactly ONE
knob in the grid (A5 = ^17) and drives four effects. `att` was on **^16 = A4 = d12's
Ardour-learned LEVEL knob**, so one knob moved an Ardour track gain AND d9's attack — the
CC77-goes-to-silence class. Moved to ^20, idle in this track, verified.
`lesliebus` on ^18 and `chorus` on ^19 borrow d10's and d11's fx knobs; both orbits are
undeclared here so they work, they are just not d9's.
- [ ] **STILL OPEN, and structural:** d9's `mask` rides **^58 = BT6 = d6's own gate**, and
d6 IS declared in this track — one button, two orbits. d9 has **no button slot at all**
in the grid, so there is no legal destination: this is gSel work, not a rename. pvlint
does not flag it because PV008 only looks at orbits 1-8.
- [x] **`sept1` "TODO fix to new controller" is already done** — 0 moves, 0 overflow, 0
FIXMEs after the remap. If it still feels wrong at the desk it is not a mapping problem.
- [x] **Both migrations verified against the baseline** — `silent-eval --seeded` re-run
over all 703 files after each, diffed per file against
`docs/2026-09-22-catalog-playability.tsv`: **zero verdict changes, twice**. 597 playable,
85 compile-fail, 21 silent-orbit, and not one file swapped sides.
- [x] **midiviz has a sun theme, a menu and a tray** (949b992) — `--theme {dark,light,sun}`,
`d` cycles, right-click menu, tray to hide into, choices persisted at
`$XDG_CONFIG_HOME/parvagues/midiviz.json` and unkillable by a corrupt config. Dark proven
pixel-identical to before. **The tray itself is UNVERIFIED** — headless has no status
area, so only the degrade path was proven. Look for the icon before relying on it.
- [ ] **midiviz state layer** — design in `docs/2026-09-22-midiviz-state-wire.md`. The
readout cannot show rest state from the wire (no readback, deltas, nothing at boot); the
driver owns the values and would publish them as one small atomically-replaced file with
a seq counter. First slice is the three DJFs as three zones with the **centre detent**
drawn, since zero is 64 and not 0. Post-gig. The Pulsar-style feed needs no new producer:
`~/.cache/parvagues/eval-events.jsonl` is already written, one line per ctrl+enter.
- [ ] **Post-gig cleanups**: 90 files carry 114 duplicate FIXME lines from the old
non-idempotent migrator (fixed at the source, the existing ones are cosmetic) · 36
PV011 bus-slot collisions remain outside the set · `tools/lcxl3-driver.py`'s `parse_bpm`
collapses into `setlist_samples.declared_bpm` · corpus pvlint 3718 warnings, mostly
PV013 missing family mutes, which `tools/fix-mute-roles.py --fill` can do in bulk.
- [ ] **techno_orage d2** (line 158) silent even seeded — fix or cut. PLN's call.
- [ ] **Preload gaps**: `bass2`, `humps` lazy-load mid-set. Regenerate after the arc is picked.
- [x] **The picker now holds the whole catalog** — the Set Builder showed 14 hand-written
candidates while the sweep had already measured all 703 tracks, which made the library
look 14 deep. `tools/gen_setbuilder_catalog.py` generates `armada/setbuilder-data.js`
from `docs/2026-09-22-catalog-playability.tsv` (the sweep, now durable in the repo — it
only lived in a scratchpad), the driver journal and `preload.scd`. Every row carries
verdict, area, play count, and how many of its sample banks are warm. 597 playable
tracks, filterable by text / area / played-here / fully-warm; 21 silent-orbit behind a
toggle; 85 compile-fails not listed. Per-area chip counts cross-check against the doc
(midi/nova 181, collab 98, techno 57, hip 41, boeuf 36). Ids are now repo-relative
PATHS, not stems: 16 stems collide across folders (`major_nostalgia` in chill and
techno, all four FFF tracks in two trees), and a stem key silently merged them into one
card. A one-line migration maps any previously-stored stem to its path, so a pick made
before this change survives.
### Play history — the real candidate pool (discovered Tue 22)
`journalctl --user -u lcxl3-driver` logs every track load. Last 5 days, 12 distinct tracks:
techno_orage(2), rose_rouge(2), piment_bresilien(2), you_my_sunshine, sunny_side_up,
something_about_drums, sept1, quand_on_decolle, jeudrill, ete_a_mauerpark, cafe_glace,
bombe_dj. That is a 2h set at ~10 min each, chosen by PLN's own hands — a stronger signal
than git-modified files. Five of them (rose_rouge, sunny_side_up, quand_on_decolle,
something_about_drums, bombe_dj) were outside the overnight sweep and are being validated.
Catalog for reference: 703 `.tidal` files under `live/`.
## Tooling pass — one launcher, one gate (Tue 22 night, DONE)
`cd9d0d8` `7c0d40e` `4d8e3e2`. Two verbs: **`gig-up.sh` DOES, `tools/check-gig.py`
PROVES.** The gate is 23 probes as a TABLE (command, fix, severity, layer, preconditions)
plus the machine layer absorbed through `gig-preflight --json` — 39 checks, one report, one
exit code. The launcher now ENDS by calling it, so the path PLN presses and the path the
Bridge presses run the same gate. `tools/gig-up.sh` keeps a superseded banner and stays
unrun until after the gig (nothing is deleted before its replacement has run for real).
Measured: **16.5s full, 3.4s `--fast`** (what the launcher uses), 0.3s machine layer.
39 tests in `tools/tests/test_check_gig.py`; suites 510 passed, 2 failed (both pre-existing).
**Five bugs it surfaced, each verified against the failing leg, not the passing one:**
1. 🔴 **preload was being rewritten from the OPAL set on every launch.** Default was
`setlist_opal2026.txt` (Sept 6). Measured **132 banks → 45**, leaving **41 of Thursday's
banks to be read off disk mid-set**. The set is now named once at the top of the launcher
and read by both the plan generator and the gate; generation converges instead of
clobbering, so the deliberate 132-bank union is left alone.
2. 🔴 **the launcher span raw `sclang`, not `parvagues-sc.service`** — so it had no
journal, no rlimits, no supervision and no OOM protection (the 2026-08-15 death). Only
the OTHER gig-up started the unit, so unifying the two is what exposed it.
3. **`LCXL present` said the desk was plugged in while it was not** — it grepped
`aseqdump -l`'s PORT column, where `lcxl3-driver` publishes a virtual output named
"ParVagues LCXL3". The gate was matching our own driver. Now anchored on the client name.
4. **`setlist compiles` proved backlog.md's 15 tracks, not the 17 being played.**
`--setlist armada/setlist_thu24.txt` scopes the compile + preload probes to this gig.
5. **`SC -> Ardour` would have printed NO-GO on every `--headphones` launch** (gated on
scsynth, but its real precondition is Ardour). Preconditions are per-probe; an unmet one
is **SKIP**, never FAIL. Also: `gig-preflight`'s "mk3 LEDs unimplemented" warning has
been untrue since `lcxl3-driver` took over painting, and `gig-log preflight` — the
armed-global check, the one that found gMask at 127 for 74 minutes — could not block
anything because it lived only in the launcher's report. It is a blocking probe now.
- [ ] **YOURS, once, tonight: press the launcher from the desktop icon.** Everything above
is proven by running the gate directly; the launcher's new blocks have never executed.
**Window closes silently = GO. Window stays open = read the NO-GO block, it carries the
fix for each line.** Two probes could false-NO-GO on a genuinely cold boot and did not
today: `gig-log recording` (asserts a session file written in the last 20s, right after
the unit starts) and `audio graph` (blocking, and it runs on the `--headphones` path for
the first time). If either fires on the first launch, suspect the probe's assumption
before the rig.
## Tue 22 — late. The launcher had never launched.
The correction to the section above, and it matters more than anything else on
this page: **"window closes silently = GO" was wrong.** The only thing
`~/.cache/parvagues/gig-up.log` had ever contained was gig-up's own usage text —
`--help` prints the options and exits 0, and the window script closed on that
zero exactly as it closes on a good launch. So the rig had never been started by
the icon at all, which is why the LCXL stayed dark: no launch, no paint step.
Fixed in `tools/gig-up-window.sh`: the fence is now "did the launcher reach its
own last line" (`gig-up done`, printed on every real path), not the exit code.
A zero with no marker holds the window, names the symptom and exits 2. Tests on
both ends of that coupling, so rewording gig-up's last line fails loudly instead
of silently holding every launch.
### Wednesday morning (23rd), in this order — NOT gig day
1. **`systemctl --user start lcxl3-driver`** — the one thing left undone
tonight; the sandbox refused to start a unit. The board IS on the bus
(`client 24: 'LCXL3 1' [type=kernel]`), so it will come up this time. Then:
- `aconnect -l | grep ParVagues` → the translated port exists
- `ls ~/.cache/parvagues/surface-state.json` → **has never existed on real
hardware**; this is the state wire's first live render
- look at C1/C2/C3 in midimon: the three DJF strips, detent at 64
`gig-up.sh` already restarts this unit when the board is present, so a real
launch covers it — but a real launch has not happened yet.
A row-D strip in midimon is **Ardour's** reported fader, not the board's: the
driver has no readback for those, so it publishes nothing for a fader nobody
has reported and the cell stays a dot until Ardour echoes one or you move it.
A jump on first touch means pot pickup was out of sync, not that the strip
was wrong.
2. **Press the launcher icon once** and watch what it does now. A window that
holds is now information, not a failure.
3. **`tools/check-audio.py`** — reads the box, changes nothing. Four warns
expected; `docs/2026-09-22-audio-profile-stuck.md` has the whole story and
the ordered repair. Do NOT restart wireplumber before the rehearsal.
4. Then the 2h arc with Ardour recording, which is still the gate.
### midimon — what was actually wrong
Nothing, in the drawing. With `lcxl3-driver` down, midiviz falls through
`WATCH_PREFERENCE` to the raw `LCXL3 1 DAW` port and reads v3 numbering: row A
lands on row A by coincidence, row C lands on row **B**, row F's buttons land on
row **C**, and the faders (v3 CC 5-12) are in no cell at all — so they became
the matrix rain, which only ever fired for CCs the grid does not own. Exactly
"only moves on abc, D dead, EF wrong, rain on the faders only". Table and
reasoning in `docs/2026-09-22-midiviz-state-wire.md`.
Shipped anyway, because both were real gaps: the state layer now covers all 48
cells (E latches show sticky, faders show their resting position before they are
touched) and the gutter rains corpus tokens (`^42`) for gridded controls.
### Audio — diagnosed, tooled, one step held back
`pavucontrol` offering only Pro Audio is two unrelated causes: a **saved pin** in
`~/.local/state/wireplumber/default-profile` (which WirePlumber restores before
its own pick — the reason it never self-corrects), and the laptop codec's
**profile list built without its UCM verbs** (fixable only by re-probing the
device, i.e. a wireplumber restart). `tools/check-audio.py` reports both, is in
the gate as `audio profiles` (ADVISE), and `--fix` **refuses** the repair that
would leave the box with no sink at all — which is what the textbook fix would
have done tonight.
Held back deliberately: no wireplumber restart, no live profile change. Ardour
is up, the four Pro Audio outputs are the only sinks on the box, and a silent
machine found in the morning by someone who did not make the change is worse
than a wrong profile.
## Learnings (Sun 21 night)
- LCXL3 protocol has no LED/OLED readback — ghost file (`/tmp/lcxl3-oled-ghost.txt`) is the
only sent-side truth.
- Raw second-client `set_text` writes did not visibly render (A/B test, PLN half-watching) —
consistent with H1 (bare set_text needs bring-up); probe will settle it.
- LED rings update instantly; OLED glass ~2 Hz effective. Rings = fast channel, OLED = state.
## Wed 23 morning — the tripwire day (gig TOMORROW evening)
Three root causes found before 09:30, all evidenced, none of them what we thought.
### 1. The LCXL3 driver has failed 1077 times and nothing ever said so
`journalctl --user -u lcxl3-driver` — **one** distinct message, 1077 times:
`lcxl3: no LCXL3 DAW output port found.` Restart counter reached **497**.
`StartLimitIntervalSec=0` is doing its job (retry forever, bind on replug), so
this is not a systemd fault — it is the surface being absent, loudly, into a
journal nobody reads.
Right now the port IS there (`lcxl3.py ports` resolves
`DAW out='LCXL3 1:LCXL3 1 DAW In 24:1'`), and the unit is `inactive (dead)` —
a clean stop at Sep 22 19:55:31, not a start-limit hit. So it starts fine today.
- [ ] **YOU: `systemctl --user start lcxl3-driver`** (sandbox refuses unit starts).
Then `aconnect -l | grep ParVagues` and check C1/C2/C3 in midimon.
- [x] **The gate's hole** — `Probe("LCXL LEDs", ...)` does check
`is-active lcxl3-driver.service`, but carries `needs=("scsynth",)`, so it
**SKIPs whenever the rig is cold** — exactly when you are setting up. The
painter has nothing to do with SuperDirt. Fixed: the probe now runs cold.
- [x] **Crash-loop detector** — a unit that is retrying forever looks identical
to a unit that is fine, from outside. The gate now reads the restart
counter (`NRestarts`) and says so.
### 2. Audio is on the NVIDIA GPU because two hand-set pins say so
`~/.local/state/wireplumber/default-profile`:
``` ```
alsa_card.pci-0000_00_1f.3-platform-sof_sdw=off <- the LAPTOP card. OFF. 17:00 talks end. stage is yours. sound test.
alsa_card.pci-0000_01_00.1=pro-audio <- 01:00.1 = NVIDIA 22bc 18:00 YOU PLAY
20:00 stop. DJ Gredine takes over.
``` ```
So: Speaker / Headphones / HDMI 1-3 are missing because **the card that carries ---
them is pinned off**, and the four `pro-output-*` sinks are the **GPU's HDMI
audio** pinned into pro-audio. WirePlumber never chose either — its
`findBestProfile` skips `pro-audio` by name. A human set both.
The SOF card's profile list also lacks `HiFi` until a device re-probe
(`alsaucm -c sof-soundwire list _verbs` says `HiFi`; PipeWire offers only
`off`/`pro-audio`), so clearing the pin alone is not enough.
- [ ] **THE WINDOW IS NOW** — scsynth dead, Ardour closed, nothing to break. A
WirePlumber restart with the rig up kills every SC->Ardour link
(`project_travel-rig-headphones`). Do it cold, today, not tomorrow.
Five ordered steps in `docs/2026-09-22-audio-profile-stuck.md`.
- [ ] **OPEN QUESTION for you:** did you pin the laptop card `off` yourself? If
you set it *because* Speaker/Headphones had already vanished, then `off`
is a symptom and the profile disappearance is the disease.
### 3. WATCH_PREFERENCE: belt AND suspenders (his call, 2026-09-23)
"in my perspective, we would have always cell and rain?" — right. midiviz # DO THIS, IN THIS ORDER
prefers the driver's translated `ParVagues LCXL3` port and falls through to the
raw `LCXL3 1 DAW` port, where v3 numbering lands in the wrong cells. The viewer
should translate for itself when it is on the raw port, so the picture is
correct whether or not the driver runs.
- [x] The v3->v2 table is now canonical in `lcxl_grid.py`; midiviz translates - [ ] **1. Bluetooth off.**
on the raw port. The driver's own copy is left untouched (it is the
nervous system, one day out) and a test asserts the two never drift.
### Still the actual gate: the 2h arc bluetoothctl power off
- [ ] **YOU: perf regime has regressed** — `epp=power`, `gov=powersave`, Why: a phone reconnecting restarts the audio system. That kills every
**PL1=15W**, and **457 xruns/hour**. `max_perf_pct` is 100 (you did that), cable in the graph, mid-set. Venue = 200 phones.
but EPP and the package power cap put the box at 854MHz. Every audio
measurement today is untrustworthy until this is fixed:
`sudo cpupower set --epp performance` +
`sudo /usr/local/sbin/thermal-mode-apply aggressive`
- [ ] **YOU: `systemctl --user restart parvagues-sc`** — loads `s.latency 0.1`
(still 0.2 in the running process, edit never went live) AND warms the
132-bank preload. ~15s of silence, safe while cold.
- [ ] **YOU: docker is running** — `sudo systemctl stop docker.service`.
- [ ] **YOU: the 2h run with Ardour recording.** Unmoved since Sunday. It is the
only check that has never been run, and the only one that can still
surprise you.
## Wed 23, 10:40 — the rig is green and the rehearsal is the whole remainder - [ ] **2. Plug the UMC box in BEFORE you open Ardour.**
`check-gig`: **0 fail, 3 warn, 39 ok, 1 skipped.** `xrun rate 0/hour` — the Why: Ardour reloads the ports it saved last time. It does NOT follow
457/hour reading this morning was the dead NVIDIA sink, not a perf problem. whatever you plugged in after. Plugged in ≠ being used.
SuperDirt: `PRELOAD: 132/132 banks OK in 6.8 s`, server ready.
Fixed today, all pushed (a606d99): the audio (laptop card back on HiFi, NVIDIA - [ ] **3. Check the sound actually goes to the UMC.**
pin retired), the LCXL3 driver (started; its 1077 failures were a resolver
looking for the ORIGINAL board's name), midiviz (v3 map moved into the grid, so
any reader of the board is right), the gate's painter probe (was skipping every
cold run), gig-log's MIDI leg (had never bound a port in nine sessions), and a
stale-build detector.
### The three remaining warns, none blocking tools/check-audio-graph.sh
pw-link -l | grep -A3 'ardour:Master'
- **audio profiles** — `50-alsa-config.conf` is 0.5 format on an 0.4.17 You want to see the UMC in there. If you see `sof_sdw` or `pci`,
install, so it is dead config that looks like protection. Post-gig. that's the laptop speakers — re-point Master in Ardour.
- **cpu governor** — daytime mode on a work laptop. `gig-up` does the perf pass.
- **monitor path** — headphones fold shows 2/28 links, d2-d14 unreachable
without Ardour. Expected with Ardour closed; the rig routes through it.
### YOURS, and it is the only thing left - [ ] **4. Master fader up with the mouse. Then Ctrl+S.**
- [ ] **The 2h arc, with Ardour recording.** This afternoon, or tonight after Master has no knob on the LCXL3, so it's mouse-only. Save while the
the speaker dinner. Never once run. Everything else is green. faders are where you actually play. That save is the only way any
- [ ] **Plug the UMC202HD for the last stretch** and run tool can check the mix later.
`tools/check-audio-graph.sh` with it present. Thursday's output path, and
the only one nobody has tested — the same UCM failure that ate the sof
card's HiFi can hit the UMC's when it enumerates.
- [ ] **Master fader**: `check-mix` reads the SAVED session, and Ardour OSC is
still off, so Ctrl+S before trusting a green fader line.
- [ ] After the run: the MIDI log is now real. `~/.local/share/parvagues/gig-log/`
carries `cc` records with `n / v0 / v1 / lo / hi` per control per second,
in corpus numbering — so "minute 23:30 the bass is too saturated" is
answerable from the file afterwards.
## Wed 23, midnight — venue seen, no rehearsal. Thursday is the plan. - [ ] **5. Play.**
PLN back from the venue: "it looks so good". **No soundcheck was possible and he ---
chose not to run the 2h arc at midnight** — "so i'll trust myself 🪷". That is a
deliberate call, recorded as one, not an omission.
**Consequence, stated plainly: the 5pm-6pm window on Thursday (end of talks → # IF SOMETHING GOES WRONG
doors) is the ONLY sound test this set will get.** One hour, two-hour set.
So that hour is for LISTENING, not for work. Nothing below is allowed to eat it.
### Thursday, before 5pm — machine work, off the critical hour **No sound at all**
- [ ] `tools/gig-up-window.sh` from the desktop icon. One press. It restores the tools/check-audio-graph.sh
fader baseline in the Ardour-closed window, starts the driver for the
board that is actually plugged, warms the 132 banks, and ends by running
the gate. Hold the window if it says `exited 0 WITHOUT LAUNCHING`.
- [ ] **Plug the UMC202HD FIRST**, before gig-up, so Ardour restores its ports
with the interface already present. Ardour does NOT follow the PipeWire
default sink.
- [ ] `tools/check-audio-graph.sh` **with the UMC on the bus** — the one path
nobody has ever tested. Expect it to be tolerant if the UMC is absent;
that tolerance is exactly why its silence proves nothing today.
- [ ] `tools/check-stale-units.py` — cheap, and it catches the class that cost
a pass on Wednesday.
- [ ] Bring the **proper Dell charger**. PL1 sat at 15W on the travel one, and
every audio measurement is untrustworthy under a power cap.
### 5pm-6pm — the hour itself Then look at where Master is pointing. 9 times out of 10 it's Ardour
still wired to the laptop card.
- [ ] Ears only. Play the opening of each movement and the two transitions you **Ardour disappeared**
are least sure of. Set the **Master fader**, then **Ctrl+S** — `check-mix`
reads the SAVED session and `use-osc=0` means no tool can see the live desk.
- [ ] Confirm the MIDI log is actually capturing before you rely on it after:
`grep -c '"k":"cc"' ~/.local/share/parvagues/gig-log/$(ls -t ~/.local/share/parvagues/gig-log | head -1)`
A number > 0. Nine previous sessions were empty and their headers all
claimed otherwise.
### After the gig Relaunch it **from the ParVagues tray icon**. Not the desktop icon.
The tray one gets no time limit; the desktop one gets a 200 ms leash and
that leash is what killed Ardour last night.
The log is the instrument now. `cc` records carry `n / v0 / v1 / lo / hi` per **One little crackle on a new sound**
control per second in corpus numbering — so "minute 23:30 the bass is too
saturated" resolves to which knob, how far, how many times. That is the input
for code/effect/DAW changes, not just a mastering fix. Then
`docs/2026-09-25-post-gig-cleanup.md`.
## Thu 24, 00:15 — one root cause, three symptoms: pv-protect puts the Ardour GUI on a realtime policy Ignore it. That's a sample loading from disk the first time. It plays
fine. Keep going.
PLN pressed the ParVagues desktop icon at 23:59, heard **kick on the laptop **Clicks and pops while you twist knobs a lot**
speakers**, saw **no Ardour**, and later **crackles**. Three complaints, one
mechanism. The launcher is NOT stale: the symlink resolves to
`tools/parvagues.desktop` → `tools/gig-up-window.sh` → the repo-root `gig-up.sh`,
and the log shows it doing every step in order.
**What the launch log says** (`~/.cache/parvagues/gig-up.log`): Known suspect, do NOT fix it tonight. Note it and move on.
``` ---
• launching Ardour (ardour) via pw-jack…
• launching Pulsar (pulsar)…
gig-up.sh: line 619: 540676 Killed setsid pw-jack ardour "…/Tidal Live.ardour"
✓ gig-up done. SuperDirt owns the LaunchControl; Ardour + Pulsar are coming up.
```
Ardour was launched, lived ~10 s, and was **SIGKILLed**. Then the script said # NEVER, NOT TONIGHT
green. `pv-protect` had touched it 2 s in:
``` ```
23:59:55 protected: ardour[540676] oom:100->-1000 sched:SCHED_OTHER/0->FIFO/80 systemctl --user restart wireplumber <- cuts every audio cable. instant silence.
``` regenerate preload.scd <- last time it warmed 0 banks instead of 137.
writing max_perf_pct by hand <- thermal-pilot overwrites it in 6 seconds.
### The mechanism (measured, not guessed) launching Ardour from the desktop icon <- that's the one that gets killed.
`parvagues-protect`'s third target is `ardour:80` — it puts **Ardour's GUI
thread** on `SCHED_FIFO/80`. Measured on the Ardour PLN opened by hand at 00:04
(PID 615285): the `ArdourGUI` main thread is FIFO/80, and gig-log has it burning
**40–85 % of a core** while the session sits idle. Meanwhile Ardour's audio
thread gets its own RT *correctly* from the backend — `AudioEngine 1` is
`SCHED_FIFO|SCHED_RESET_ON_FORK` at **83**, i.e. above the GUI, granted by
PipeWire, needing nothing from us. So the FIFO on the GUI thread buys zero and
costs three things:
1. **The SIGKILL.** `RLIMIT_RTTIME` is inherited, and it differs by who
launched you: `gnome-shell` runs with **200000 µs**, a systemd user unit with
**unlimited** (measured: gnome-shell 200000, pv-protect's own shell
unlimited). The desktop icon is a gnome-shell descendant, so Ardour launched
from it carries a **200 ms realtime budget** — and a GUI thread loading a
355 KB session with plugins blows 200 ms of uninterrupted CPU easily. The
kernel then sends SIGXCPU and, soft == hard, SIGKILL. **No kernel log line,
which is why the journal looked empty.** The Ardour PLN started by hand at
00:04 shows `Max realtime timeout: unlimited` and survived — the
discriminator holds.
2. **The crackles.** A FIFO/80 thread at 85 % of a core, on a CPU capped at
`max_perf_pct=65` with governor `powersave`, misses deadlines. gig-log's
attribution is unambiguous: in the 6 minutes after Ardour opened,
`xrun_by.ardour` went **0 → 46** while SuperCollider added ~9. And it is
**chronic, not new** — `ardour` xruns per session: 5172 (Sep 10), 4852
(Sep 18), 1114 (Sep 20), 987 (Sep 22). Nobody had ever attributed them.
3. **d1 straight to the speakers.** Not a second bug — the consequence. With no
Ardour, WirePlumber auto-links `SuperCollider:out_1/2` to the internal sink
and d2–d14 reach nothing. The gate said exactly that ("headphones: 2/28 fold
links present") and then *graded the rig as the headphones rig*, because it
infers the mode from Ardour's absence. With Ardour back up the graph is
correct again: 61 orbit links into `ardour:Tidal NN`, `ardour:Master` → the
sinks, no direct SC → speaker link left over.
### Thu morning — in this order
- [ ] **1. Make Ardour survive its own launch.** Blocks every Ardour-side item
below. **TWO root files re-arm this, not one** — fixing either alone leaves
the bug live:
- `/usr/local/bin/parvagues-protect`, the `TARGETS` line (`ardour:80`),
re-applied every 2 s;
- `/usr/local/sbin/perf-audio:565` — `chrt -f -p 80 $ARDOUR`, plus
**line 573 `chrt -f -p 79 $child` for every child**. `perf-watch`
reasserts this on its own whenever it sees drift (seen in the Bridge
journal at 09:20:15 on Sep 23: "drift: reasserted standard" →
"✓ Set Ardour (PID …) to real-time priority 80"). So an AC that only
tests pv-protect will pass at home and fail at the venue.
Two candidate fixes, both one line each, PLN's call (`sudoedit` both, then
`sudo systemctl restart parvagues-protect`):
- **(a) Drop the FIFO, keep the OOM shield.** `ardour:80` → protect
`oom_score_adj=-1000` only. Justification: `AudioEngine 1` already runs
FIFO/83 from the backend, so the GUI promotion protects nothing that
makes sound. This also fixes symptom 2. **Recommended.**
- **(b) Lift the budget before promoting.** `prlimit --pid $pid
--rttime=unlimited` before the `chrt`, exactly the shape the script
already uses for `memlock_unlimited`. Fixes symptom 1 only; leaves an
85 %-of-a-core GUI thread at FIFO/80 in the graph.
AC for either: launch from the **desktop icon** (the path that failed —
launching from a terminal or the Bridge does not reproduce it), and
Ardour is still alive 60 s later with `xrun_by.ardour` flat.
**Epistemic status, so nobody over-trusts this.** The kill mechanism is
*consistent with every measurement* — SIGKILL with no kernel line, Pulsar
from the SAME launch carrying `rttime=200000` (proving the icon chain
inherits gnome-shell's budget), the hand-launched survivor at `unlimited` —
but it has **not been reproduced**. The AC above IS the reproduction. It is
also non-deterministic by nature: it needs one >200 ms uninterrupted GUI
burst, so earlier icon launches that survived do not falsify it.
The xrun half is measured (`xrun_by.ardour` 0→46, and four prior sessions)
but the FIFO/80 GUI thread as its *cause* is a hypothesis, confounded
tonight by the 65 % cap, `powersave`, a session mid-load, mpv and qjackctl.
What gives it teeth: `/proc/sys/kernel/sched_rt_runtime_us` is at the
default **950000/1000000**, so once the realtime tasks on a runqueue exceed
95 % of a period the kernel throttles **every** RT task on it for the
remaining 50 ms — which is 2.3 quanta at 1024/48k. A GUI thread eating
85 % of a core at FIFO/80 is exactly how `AudioEngine 1` at 83 gets
stalled by something *below* it.
- [ ] **2. The CPU cap, still unpaid** (third session running). Measured now:
`max_perf_pct=65`, governor `powersave`, EPP `balance_power`, and gig-up's
own line claimed "thermal-mode performance holding" while the gate read
the opposite — that perf line is **stale and must not be trusted**.
`echo 100 | sudo tee /sys/devices/system/cpu/intel_pstate/max_perf_pct`
plus `sudo cpupower set --epp performance`. Until this is paid, no xrun
number from the rehearsal means anything.
- [ ] **3. `systemctl --user restart parvagues-sc` at the START of the
rehearsal** — `s.latency` 0.1 is still only in the file; the running
process holds 0.2. AC after 10 min of play:
`journalctl --user -u parvagues-sc -b | grep -cE '\blate [0-9]'` == 0.
- [ ] **4. Master fader, then Ctrl+S.** `check-mix` reads the SAVED session and
`use-osc=0`, so nothing outside Ardour can see the live desk. Enable
Ardour OSC while you are in there and the blind spot closes for good.
- [ ] **5. Quantum 512 listen test**, only after items 1 and 2.
- [ ] **6. Full-set run**, late-count + xrun attribution from gig-log.
### The gate bug behind the false green (post-gig, write it down now)
In stage mode, an Ardour that **gig-up itself just spawned** and that is dead at
gate time must **FAIL**, not `SKIP`. Tonight the gate reasoned "Ardour is not
open → this must be the headphones rig" and graded a mode PLN was not playing,
then `gig-up.sh` printed `✓ gig-up done … Ardour + Pulsar are coming up` about a
process it had already been told was `Killed`. The launcher knows the PID and
knows whether the spawn survived; it must say so. "A clean exit is not a launch"
is already written in `gig-up-window.sh` — this is the same lesson one layer in:
**a spawn is not a process.**
## Thu 24, 00:40 — ^41 twice after a track change is REAL, and the MIDI log is dead again
**PLN was right.** Moving salut_nu → love_first he suspected the kick gate needed
two presses to respond. It does, and `carry_values()` says so on purpose:
```python
latches = sum(1 for on in self.latch.values() if on)
self.latch.clear()
for v2 in self.grid.BUTTON_CCS:
if v2 in self.values: self.values[v2] = 0
print(f"… {latches} latches cleared (no CC sent)")
``` ```
Its own docstring: *"Emitting to a gate is the mutebomb; the latch state is reset ---
in the DRIVER so the LEDs stop lying and the next press means ON, but nothing is
sent."* So after the change the driver believes `^41` is off while **SuperDirt
still holds 127**. Press one toggles off→on and emits 127 — the value SC already
has, so nothing moves. Press two emits 0 and the sound finally follows.
Timestamped evidence of the mechanism firing on his exact switch:
`00:38:41 lap reset · 2 effect knobs -> 0 · 3 carried (DJF + tempo) · 2 latches
cleared (no CC sent)`, immediately before `00:38:51 track ~ love_first`.
The docstring's justification is the part that is backwards. It clears the latch
"so the LEDs stop lying" — but after the change the gate IS still open in SC, so
a lit LED is the **truth** and the dark one is the lie. The clear makes the LED
match the new track's *default* instead of the sound's *state*.
- [ ] **POST-GIG, not before** (this is the mute path — the worst thing to break
on a gig day). The fix the driver already has the information for: carry
the latch for buttons the NEW track also maps, and clear-**and-emit-0** for
the ones it does not. A gate nothing is listening to can be zeroed with no
mutebomb; a gate both tracks read keeps its state, so the LED and the sound
agree and one press closes it. `self.mapped` per track is the discriminator.
- **Thursday workaround: close your gates BEFORE you switch tracks.** Then the
clear is a no-op and there is nothing to double-press. The journal grades you
for free — `0 latches cleared` is a clean switch, `2 latches cleared` is two
gates you will have to press twice. Tonight's switches read 0, 0, 0, 2, 3, 3.
### And the reason this had to be read off the driver's journal: gig-log binds ONCE and never re-resolves
Zero `cc` records in `gig-20260923-103812.jsonl` — 16227 `s` samples, 28 `eval`,
9 `track`, and **0 `cc`**. The
`mbind` line names the culprit:
```
"k":"mbind","p":"131:0","name":"ParVagues LCXL3","v3":false,"xlate":false (written 10:38:12)
```
The driver publishes a PAIR, and the two names differ by a suffix:
```
client 130: 'RtMidiOut Client' 0 'ParVagues LCXL3 ' <- the translated output (midiviz reads this)
client 131: 'RtMidiIn Client' 0 'ParVagues LCXL3 FB' <- the feedback input. gig-log bound THIS.
```
**CORRECTED 00:59, and the first reading was wrong — keep the correction, it
changes the fix.** The initial call was "it bound the FB port", on the grounds
that `find_seq_port` matches port names by substring and `ParVagues LCXL3 FB`
contains `ParVagues LCXL3`. Measured afterwards: **`aseqdump -l` lists SOURCES
ONLY**, and the FB port is an input, so it does not appear in that listing at all
(`aseqdump -l | grep -c FB` = 0). The resolver could never have picked it. `131:0`
WAS the correct output port — at 10:38:12.
The real fault is one word in the docstring: *"re-resolved every connect"*. It
only re-resolves when the aseqdump connection **drops**, and it never dropped.
The driver's ALSA client id moves on every republish — tonight alone `131 → 130 →
133` — and when the client underneath vanishes, aseqdump keeps running,
subscribed to an address that no longer exists. No drop, no reconnect, no
records. Bound once at 10:38 and deaf for fourteen hours.
- [x] **Hardened the matcher anyway** (exact port-name match beats a substring,
and the `FB` suffix is excluded the way `HUI` already is). Harmless and
more precise, but say plainly what it is: **not the fix**. It resolves
correctly *today* and would have resolved correctly at 10:38 too.
- [x] **Rebound for tonight** — gig-log restarted 00:59:26,
`"k":"mbind","p":"133:0"`, which is the live port.
- [ ] **The actual fix, post-gig: LIVENESS, not name matching.** Re-run
`find_seq_port` on a timer (or watch the seq graph) and rebind when the
bound address is no longer in `aseqdump -l`, whether or not aseqdump
noticed. A subscription that outlives its port is the same stale-binding
family as everything else on this rig — and here the *reader* had no way to
know, because a dead seq subscription is indistinguishable from a quiet
surface.
- [ ] **One-line class fix** (safe — gig-log makes no sound): rank an EXACT name
match above a substring one, and exclude the pair's other half the same way
`HUI` is already excluded — `and " FB" not in line`. Then restart gig-log
and confirm within seconds of moving one knob that `cc` records appear.
**Verify by the records, never by the header**: `"midi": true` has been
lying for every one of them, because `available()` only asks whether
aseqdump is installed.
**COUNT CORRECTED 01:55** (a subagent caught it, and the real numbers tell a better
story). `cc` records per session: **nine straight zeros** Sep 6 → Sep 22 (no `mbind`
at all — the "Launch Control XL" name era), then `gig-20260923-101102` with **35**,
then `gig-20260923-103812` with **0**, then tonight's `gig-20260924-005926` with
**145**. So it was never "ten in a row": the September name fix **worked**, and the
10:11 session proves it. The 14-hour 10:38 session recorded nothing for a *different*
reason — it bound a live address that later died under it. Two failures, not one, and
the second is the liveness bug that is still open. This is also independent evidence
for the corrected diagnosis above: a resolver that could only ever pick the wrong
port would not have produced 35 records an hour earlier.
## Thu 24, 01:20 — the cap is thermal-pilot's rung-1 FLOOR, proven by the rig's own hand
I told PLN to stop `thermald` and lift the cap by hand. **Both halves of that were
wrong, and the correction matters more than the advice.**
**thermald is innocent.** It is `--adaptive` and its
`/etc/thermald/thermal-cpu-cdev-order.xml` does list `intel_pstate`, which is why
it looked guilty. But it was **stopped** at 00:40 and `max_perf_pct` stayed at 65
for the next forty minutes. A writer that is not running is not the writer.
**Restart it** (`sudo systemctl start thermald`) — the box has been sitting with
no thermal daemon since 00:40 for no reason, and that one is on me.
**The writer is `thermal-pilot.service`**, the rig's own ladder, and the proof is
its own log. At 01:12, using the sanctioned NOPASSWD helper:
```
BEFORE cap=65 epp=balance_power
sudo -n thermal-mode-apply performance -> cap=100 epp=balance_performance
t+6s thermal-pilot: drift: live knobs no longer match rung 1 -- reasserted
AFTER cap=65 epp=balance_power
```
**Six seconds.** So the mangled paste cost nothing: `echo 100 | sudo tee` would
have been undone before the prompt came back.
**And it is not a leftover to clear — it is the design.** From the sources:
- `thermal-pilot`: `rung_knobs 1` = `"0 $RUNG1_PCT balance_power"`, and
`RUNG1_PCT=${RUNG1_PCT:-65}`. **65 is rung 1's definition.**
- `thermal-pilot`: `profile_params performance` = `"1 2 ${PERF_FAN_SOFT:-2600}"` —
floor **rung 1**, ceiling rung 2, noise brake **2600 rpm**. Its own header says
it plainly: *"performance: floor of capped turbo, braked at 2600 rpm"*.
- The fan idles near **2800 rpm**, so the brake is always armed. Tonight the pilot
climbed `1 -> 2` **six times** and was braked back within 5 s every single time:
`rung 2 -> 1 (noise brake: fan 2802 rpm > 2600 for 30s)`.
Under `MODE=performance`, **65 % is the best this box will hold while the fan sits
above 2600 rpm.** No hand-written value survives it.
### The upstream cause is probably the CHARGER, and TODO already said so
`thermal-mode-apply performance` reported `PL1=25W PL2=50W … budget: 45W USB-C PD
source`, while `/etc/thermal-mode.conf` records `STOCK_intel_rapl_0_PL1=45000000`
(45 W) and `PL2=90000000`. So the box is on the **USB-C travel charger**, running
at roughly half its stock power budget. A halved PL1 means the same work is done
slower and hotter, the fan climbs past 2600, the noise brake fires, and the pilot
pins rung 1. *"Bring the proper Dell charger"* was already on the 5pm list as a
measurement-hygiene item; it is now a suspected **cause** of the cap.
**Test it first thing, because it may cost nothing:** plug the Dell barrel
charger, `sudo -n /usr/local/sbin/thermal-mode-apply performance`, wait 30 s, read
`max_perf_pct`. If PL1 reads 45 W and the fan settles under 2600, the cap holds by
itself and the whole item closes with a cable.
### If the charger is not enough, it is a choice — and it is PLN's, not mine
I have been wrong about this writer twice tonight. So: the options, with the
trade, and no pick.
- **(a) Raise the brake.** `PERF_FAN_SOFT` is an env default in `thermal-pilot`,
so a root drop-in on `thermal-pilot.service` with
`Environment=PERF_FAN_SOFT=3400` lets rung 2 hold. Cost: a louder laptop. At a
gig with a PA, fan noise is free — this is the cheapest real fix.
- **(b) Raise the floor.** A drop-in with `Environment=RUNG1_PCT=100` makes rung 1
harmless. Cost: the ladder loses its middle step everywhere, including on
battery at a café. Blunter than (a).
- **(c) Stop the pilot for the set.** Its exit trap writes rung 0, which is
`"1 100 power"` — **cap 100 but TURBO OFF**, which is not what a set wants.
Read the table before reaching for this one; it is not "uncapped".
- **(d) Leave it at 65 %.** Measured tonight: with Ardour's GUI threads demoted,
xruns went to **0 in 14 minutes** *at 65 %*. The cap may simply not be the
binding constraint once the RT bug is gone. Cheapest of all, if the rehearsal
agrees.
`gig-up.sh` no longer has an opinion here, but it now tells the truth (see below).
### Landed tonight, no root needed
- [x] **`gig-up.sh ensure_perf` reads the KNOBS, never the mode file.** It used to
print *"thermal-mode performance holding"* from `MODE=` in
`/etc/thermal-mode.conf` while the gate, twelve lines later in the same log,
FAILED on `max_perf_pct=65`. Both were right: the file records the last
intent, not the live posture. It now asserts the gear, sleeps 8 s so the
pilot has reasserted, reads `max_perf_pct` / EPP / `no_turbo` / PL1, and
either says **VERIFIED** or names the owner — *"owner = thermal-pilot
(profile 'performance'), and it reasserts within ~6s"* — plus the PL1-vs-45W
charger hint. **No sudo line to paste, and no memory to rely on.**
- [x] **`tools/fix-ardour-rt.sh`** — the #1 blocker, as a script instead of
instructions. Dry-run by default, `--apply` to write, `--revert` to undo,
idempotent, backs both files up, refuses any file whose expected text it
cannot find, and `bash -n`s the result before restarting anything. It patches
**both** writers (`parvagues-protect`'s `ardour:80` -> `ardour:0` plus the
`prio > 0` guard that a 0 requires, and `perf-audio`'s two Ardour `chrt`
calls at ~565/573), restarts `parvagues-protect`, restarts `thermald`, and
prints the acceptance test. Ardour keeps its OOM shield, its ionice and its
memlock; scsynth/90, sclang/85 and pipewire/95 are untouched.
**Verified before you run it:** all four patch patterns match the live root
files exactly once, and the patch was applied to *copies* — both files still
`bash -n` clean, the guard is in place, and nothing else moved.
### Thursday morning, in order
1. `sudo systemctl start thermald` — my mistake, 30 seconds.
2. `sudo tools/fix-ardour-rt.sh` (read the diff) then `--apply`. Then the
acceptance test it prints: **launch Ardour from the DESKTOP ICON**, because a
terminal or Bridge launch inherits `unlimited` and proves nothing.
3. **`sudo systemctl start parvagues-protect`** — it is OFF right now, on purpose,
which means scsynth currently has **no OOM shield and no FIFO/90**. Step 2 is
what makes it safe to turn back on.
4. Plug the **Dell barrel charger**, re-assert the gear, read the cap. Decide (a)
or (d) above with the rehearsal's ears.
5. `systemctl --user start midiviz` if you want the lens.
## 🔴 Thu 24, 01:25 — READ FIRST: `liquid_nite` (track 3 of 17) DOES NOT COMPILE
The full gate found it. Your **uncommitted** edit to
`live/midi/nova/lounge/liquid_nite.tidal` left a dangling composition operator:
```
line 46: $ midiOn "^58" (iter 4 . )
^ nothing after the dot
```
```
liquid_nite.tidal:46:21: error:
• Couldn't match type: a0 -> Pattern c0 with: Pattern ValueMap
• In the second argument of 'midiOn', namely '(iter 4 .)'
```
`(iter 4 . )` is a section of `.`, so it types as a function-to-function rather
than a `Pattern ValueMap -> Pattern ValueMap`. It is one `do` block, so **every
`dN` in liquid_nite is dead**, not just d6. You played the track fine at 00:19
(the driver logged `track ~ liquid_nite … eval=cycle 0` four times) and edited it
after, so this is a mid-thought left on the desk when you folded — you were
clearly about to chain something onto that dot.
**Two fixes, BOTH verified by `silent-eval --seeded` on copies tonight — I did not
touch your file, the choice is musical:**
- **Finish the thought or drop the dot.** `$ midiOn "^58" (iter 4)` →
`liquid_nite: ok, every declared orbit emits`. Keeps both of tonight's
additions: the new `^58 iter 4`, and the `^90 (ply "4 <8 16>")` you
uncommented. **One character. This is almost certainly what you want.**
- **Or revert the file.** `git checkout -- live/midi/nova/lounge/liquid_nite.tidal`
→ also `ok`. But it throws away both of tonight's lines, and **close Pulsar on
that file first** — a checkout under a live editor buffer is how the editor
writes the old text back over it.
Re-run `python3.12 tools/silent-eval.py --seeded --setlist armada/setlist_thu24.txt`
after, and expect 17/17. The other 16 are `ok` right now.
### The rest of the gate, triaged — 01:25, full run, 43 checks
`31 ok · 6 fail · 5 warn · 1 skipped` — and here is what each failure actually is,
because three of them are consequences of tonight's work rather than new faults:
- ✗ **setlist compiles** — the `liquid_nite` dot, above. **The only one that can
silence a track on stage.**
- ✗ **preload covers set** — **caused by tonight's parser fix, and it is the fix
working.** `extract_names` now sees `808bd`, `808cy`, `808hc`, `808sd` and
`90s_synatm`, which the generated `preload.scd` was never built for because the
old parser read them as `bd`/`hc`/`sd`. So the gate is right and the preload is
genuinely short by five banks. `tools/check-preload.sh --fix` then an SC restart
— **do it WITH PLN**, per the standing rule about that generated, gitignored,
load-bearing file (it "debuted as a crackle at the venue"). I did not regenerate
it unilaterally.
- ✗ **fader baseline** + ✗ **ardour faders** — your desk, your hands. Raise the
Master, **Ctrl+S**, re-run. `check-mix` reads the SAVED session and `use-osc=0`,
so no tool can see the live desk. Already on the 5pm list.
- ✗ **cpu energy bias** + ✗ **clock ceiling** — `thermal-pilot` rung 1, explained
above. **Note the gate's suggested fixes for these two cannot work**: `echo 100 >
max_perf_pct` is reasserted in 6 s and `cpupower set --epp performance` likewise.
Post-gig, those two fix strings should name the pilot instead of handing you a
command that loses.
- ! **xrun rate 379/hour** — **contaminated by me, disregard it.** Every burst in
this session timestamps to a command I ran: 01:02 was the SuperDirt restart
storm, 01:12 the `thermal-mode-apply` probe, 01:16 the patch simulation and the
gate itself. With the rig merely sitting, the **last 5 minutes are 0 xruns**, and
the clean 14-minute window after demoting Ardour's threads was measured *at* the
65 % cap. Re-measure with your hands on the board and nothing else running.
- ! **SC un-killable** — that is `parvagues-protect` being off on purpose. Step 3
of the morning list closes it.
### Morning list, final order
0. **`liquid_nite` line 46** — one character. Everything else can wait; this cannot.
1. `sudo systemctl start thermald` — my bad call, 30 seconds.
2. `sudo tools/fix-ardour-rt.sh` → read the diff → `--apply` → the acceptance test
it prints (**launch Ardour from the DESKTOP ICON**; a terminal or Bridge launch
inherits `unlimited` and proves nothing).
3. `sudo systemctl start parvagues-protect` — only after step 2.
4. `tools/check-preload.sh --fix` **together**, then one SC restart (which also
warms `90s_synatm` and finally loads `s.latency 0.1` — watch the `late` count).
5. Plug the **Dell barrel charger**, re-assert the gear, read `max_perf_pct`. The
cap may close for a cable.
6. Master fader → **Ctrl+S** → re-run the gate. Target: only the cap items left.
## 🕐 Thu 24, 10:30 — THE DAY'S CLOCK, from PLN
Sourced this morning **from PLN directly**, in his words: the conference "plays
until 5pm", then he "setup the main stage for a gig at 6pm-8pm", then "DJ Gredine
picksup from 8pm to 10pm". Recorded here because it is nowhere else — there is
still no AI ENGINEER entry under `~/Work/Perso/www/content/lives/2026/`, so this
file is the only written record until one exists. **Not derived, not guessed.**
| Time | What |
|---|---|
| until 17:00 | conference talks — the 17:00 sound-test window is the end of them |
| 17:00–18:00 | PLN sets up the main stage |
| **18:00–20:00** | **ParVagues — 2 h.** The 17-track setlist is ~120 min. It fits |
| 20:00–22:00 | DJ Gredine |
No "doors" time was given and none is invented. The set is **two hours**, which is
what `armada/setlist_thu24.txt` was built for — 17 tracks, ~9 min each, settled
2026-09-22.
### Morning list — status at 10:30
- [x] **0. `liquid_nite` line 46** — committed last night. Whole set re-verified
cold this morning: **17/17 ok**, including PLN's uncommitted `love_first`
(`^56` jungle_breaks:80) and `take_5_drops` (`. chop 8`) edits.
- [x] **1. thermald** — PLN started it. `active`.
- [ ] **2. `sudo tools/fix-ardour-rt.sh`** — NOT applied; `parvagues-protect:92`
still reads `ardour:80`. Needs root, so it is PLN's hands. **Ctrl+S in
Ardour first**: the acceptance test needs a quit-and-relaunch from the
desktop icon, and the currently-running Ardour was launched by the Bridge
(parent PID is `python3`, `RLIMIT_RTTIME` **unlimited**) — so this instance
is immune to the SIGKILL and proves nothing.
- [ ] **3. `sudo systemctl start parvagues-protect`** — still `inactive`, so
scsynth has no OOM shield and no FIFO/90 right now. Note the fix script
restarts protect itself, so check `is-active` after step 2 rather than
starting it twice.
- [x] **4. preload** — done, and it was a trap. See
`armada/tasks/047-the-fix-that-warmed-nothing.md` and commit `3b51877`.
`--fix` wrote `\808bd`, which SuperCollider cannot parse, so the plan
warmed **0** banks instead of 132 — and `check-preload` then reported `ok`
because its shell grep could not see the quoted rows either. Both fixed,
43 tests added and watched failing. Now **137/137 banks warm in 3.8 s**.
Graph unchanged across the restart (49 links, 39 SC ports, no leak).
- [x] **5. Dell barrel charger** — plugged, and it moved a real number:
**PL1 25 W → 45 W** (stock), the USB-C source having negotiated 45 W total.
But `max_perf_pct` is **still 65** and EPP is still `balance_power`, so the
cable was never the clock cap — that is `thermal-pilot` rung 1, as measured
last night. Half-closed: the power limit is fixed, the (a)/(d) cap decision
is still open and can wait for the sound test's ears.
- [ ] **6. Master fader → Ctrl+S** — Master is now at **−0.5 dB** (was the
complaint), but the session as last SAVED (00:14:49, *before* the 01:00
rehearsal) still shows **five orbits shut**: `Tidal 05` and `Tidal 06` at
−inf, `Tidal 10` at −63.6, `Tidal 08` at −59.9, `Tidal 04` at −33.8. He
played d8 fine at 01:00, so this is almost certainly a stale file rather
than a stale desk — but `check-mix` reads the saved session and `use-osc=0`,
so no tool can see the live desk. **One Ctrl+S settles it**, and it has to
happen before the Ardour quit in step 2 or the live desk is lost.
## 🚐 Thu 24, 11:10 — AT THE VENUE, in order
The rig is green at home. `parvagues-protect --check` exits 0, the set compiles
17/17, the preload warms 137/137. Two things could not be tested here.
### 1. The UMC202HD, first thing, before Ardour
It has **never been on this box**. Plug it in *before* launching Ardour, because
**Ardour restores its own saved ports and does NOT follow the PipeWire default
sink** — so the interface being present is not the interface being used.
```
tools/check-audio-graph.sh # this is exactly what it is for
pw-link -l | grep -A3 'ardour:Master' # is Master on the UMC, or still on the laptop?
```
Two failure signatures to recognise, both seen on this box before:
- **UCM profile lies.** A card can enumerate with a profile reporting
`available: yes` unconditionally while nothing is plugged in. `tools/check-audio.py`
detects it and refuses a fix that would leave zero sinks.
- **Ardour keeps the old ports.** Master still wired to
`alsa_output.pci-…sof_sdw…` instead of the UMC. Re-point it in Ardour, then
Ctrl+S *while the faders are where you play them*.
### 2. Bluetooth OFF before the set
New evidence this morning: the pre-fix Ardour (PID 615285) had **no realtime at # THE RIG IS FINE. Checked at home this morning.
all** — `AudioEngine 1` was plain `TS`. The one launched from the tray at 11:05
has `SCHED_FIFO|SCHED_RESET_ON_FORK/83`, exactly as the fix's rationale claims.
The likeliest way a running Ardour loses its RT grant *and* its links is a
**PipeWire/WirePlumber restart underneath it** — which, per
`project_wireplumber-churn-bt`, a phone and a BLE lamp reconnecting cause about
once a minute. **0 restarts today**, but Bluetooth is powered on and the venue is
full of phones.
``` ```
systemctl --user restart wireplumber # NEVER during the set: it kills every link set 17 / 17 tracks compile
bluetoothctl power off # do this instead, before you start samples 137 / 137 banks warm, 3.8 s
protect on, check passes
Ardour audio thread has priority, GUI thread doesn't steal it
scsynth shielded (it used to be first in line to get killed)
graph 50 cables, Master reaches the speakers
charger Dell barrel plugged, 45 W
``` ```
### 3. The relaunch question, settled Two things nothing at home could test: **the UMC box** (never plugged into
this laptop, ever) and **the room**.
**The ParVagues tray is not the desktop icon.** The tray launch's parent is ---
`perf-tray.py`, and it inherits `RLIMIT_RTTIME=unlimited` — same as the Bridge.
The desktop icon inherits gnome-shell's **200000 µs**, which is what killed Ardour
at 23:59. So if you always launch from the tray, **you were never exposed to that
kill**; the icon path was. Do not go launch from the icon tonight to reproduce a
bug you will not hit. On the tray path, the value of `fix-ardour-rt` is the *other*
half: no GUI thread at FIFO/80 starving `AudioEngine` at 83 through RT bandwidth
throttling (`sched_rt_runtime_us` 950000 = a 50 ms throttle, 2.3 quanta).
Verified on the 11:05 instance: `AudioEngine 1` at FIFO/83, **nothing at 80**, # NOT TONIGHT — tomorrow or later
graph back at 50 links / 39 SC ports, Master reaching the speakers.
### 4. One loose end, for the sound-test hour and not before - Latch double-press on the mute path
- gig-log loses its MIDI address and never looks for it again
- Chord-reset + the page/logo dawless controls
- Ardour's MIDI helper threads sit at priority 92, above the audio thread
- The 65% CPU cap: leave it, or lift it? decide with your ears, not a number
- The gate said green while the preload warmed nothing — that bug is still there
`midiUI`, `Generic MIDI` and `AutomationWatch` run at **SCHED_FIFO/92**, above ---
`AudioEngine`'s 83. That is Ardour's own doing, not ours, and it is NOT to be
touched tonight. But if xruns show up and they correlate with heavy LCXL3 knob
movement rather than with the patterns, that is the first suspect.
### Not a fault, so it does not go on a checklist *Everything written before today (1101 lines of it: diagnosis, measurements,
the whole week) is not lost:*
`check-mix` FAIL and `fader-baseline` DRIFT compare the session against an git show 50adcf4:TODO_GIG.md
**August 2 snapshot**. PLN rides the D-row up when he plays, so faders parked at
`-inf` in a saved file are his workflow, not a bug — the 11:01 save simply
recorded a parked desk. **The one number that is not explained by that is Master
at −15.5 dB** (it was −0.5 at 00:14), because no MIDI binding for Master was found
in the session (`<Binding>` count 0) or in `~/.config/ardour8/`. If Master is not
under your hand, everything arrives 15.5 dB down. Ask before touching it.
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