- 29 Jul, 2026 24 commits
-
-
PLN: "wap has atm bass on d4, yet d4 effects on 57 and 89!! Why dont i see your changes in tidal ??" ... then, on the diagnosis: "how can we trust each other with pulsar" and "cause reloads dont do it, and i could close tabs before your edits then, but its annoying ahahah". == WHAT HAPPENED == Pulsar saves the BUFFER, not the file. Its process had been running since Sun Jul 26 17:19, so every track opened before this afternoon held three-day-old text. PLN commented two lines out in wap.tidal and hit ctrl+S — and Pulsar wrote that entire stale buffer back, silently reverting EVERY control the column migration had moved: ^31 -> ^52 ^32 -> ^53 ^33 -> ^34 ^17 -> ^18 and it deleted the FIXME(#54) comment No warning, no conflict marker, no error. He noticed an hour later because a knob was in the wrong place. do_it_right.tidal was hit the same way. Two details that make this nastier than it sounds, both now written down: * "Window: Reload" does NOT fix it — that restores buffers from Pulsar's session cache, not from disk. The only reliable action is to CLOSE THE TAB and reopen. * The clobber is INDISTINGUISHABLE from a hand edit at a glance. It arrives inside a legitimate diff, mixed with real intent. Repaired by re-running migrate-columns (idempotent — it recomputes the permutation from current content), plus one hand fix for wap's d9 ^18 -> ^17 which the migrator does not cover since d9-d12 are not column orbits. PLN's actual intent — commenting out three crushbus/octer lines in wap d4 — is preserved: those were live pre- migration and he disabled them on purpose. Verified by diffing against the pre-migration commit rather than assuming. == WHY DETECTION AND NOT PREVENTION == Prevention is unavailable. Pulsar's ~/.pulsar/storage/application.json is 75 bytes of project roots — there is no way for a tool to ask which files are open, so I cannot refuse to edit an open file. And "close every tab before Claude edits" is a chore, not a system; PLN is right to reject it. So the trust mechanism is two things that need no discipline from either of us: 1. COMMIT IMMEDIATELY. Already the practice, and it is exactly why this was recoverable in two commands instead of lost — the clobber showed up as a diff against a known-good commit. 2. DETECT IN SECONDS, NOT AN HOUR. tools/check-drift.sh. == THE DESIGN LESSON INSIDE check-drift == The first version counted changed ^NN lines and called any bulk change a clobber. It then flagged my own REPAIR of the clobber as drift. That is crying wolf, and a gate that cries wolf gets ignored — the same principle already written into `gig-log preflight` about never failing on intentional performance state. The fix was to define the thing properly. Drift is not "control numbers changed", it is "control numbers moved AWAY FROM THE GRID". So the grid is measured FIRST (surface-columns --knobs, floor of 5 = the deliberate gSel overflows), and the working-tree diff is then judged in that light: bulk CC change + broken grid = DRIFT; bulk CC change + intact grid = a repair, reported and not failed. Verified both directions, which is the only way this is worth anything: positive — current repaired tree: exit 0, wap correctly read as "toward the grid" negative — restored the pre-migration wap over the top to simulate the clobber: 13 knobs out of column and 16 ^NN lines changed, both flagged, exit 1 Then restored, grid back to 5. Wired into gig-up.sh's readiness gate alongside preflight and check-mix, so the question gets asked whether or not anyone remembers to ask it.PLN (Algolia) authored -
PLN, after a change of mine reached his ears before a check did: "we gotta make things work bro we cant break as we move i thought we had clarity and confidence by now :) go on, take the longshorter path not hte immediate one". Right, and the honest diagnosis is not "be more careful". It is that this toolbox had a whole missing category of check. Everything in it is COLD: pvlint parses .tidal text silent-eval queries patterns against an EMPTY control map, in a fresh ghci surface-columns reads .tidal text check-boot loads BootTidal in a throwaway ghci check-mix reads the SAVED Ardour session All five were green all afternoon. Meanwhile gMask sat ARMED AT 127 on the live board from 15:36 to 16:50 — a global chopping the last eighth out of every bar of every orbit wrapped in gM1/gM2/gM3, which is every orbit of every track. Nothing was broken. A switch was on, and NOT ONE tool in the chain looks at the switches. PLN found it by ear, and the ear should never be the smoke detector. `gig-log preflight` closes that. It reads the live recorder's surface state and returns a verdict on the controls that can silence a whole rig from outside any pattern: gPanic armed, any gMute engaged, a DJ filter parked in a band-killing position, gMask armed if it is still active. == THREE DESIGN DECISIONS, EACH FROM A PAST FAILURE == 1. A STALE LOG FAILS. Exit 2, and distinct from exit 1 (surface unsafe) so a caller can tell "I could not check" from "I checked and it is bad". A preflight that reports SAFE because it read yesterday's session converts an unknown into a false reassurance, which is precisely the shape of every bad afternoon this rig has had. is_live() already existed; this is the first thing to gate on it. 2. gMASK'S MEANING IS READ FROM BootTidal.hs, NOT HARDCODED. ^41 was a global gate this morning; since 37857225 it is d1's gate and gMask is `id`. Same CC, opposite verdict. Hardcoding either answer would be wrong half the time and would go stale exactly the way every other cached binding here has (feedback_stale_binding_pattern). An unreadable BootTidal assumes the mask is STILL ACTIVE — the cautious read, because guessing "retired" silences a real FAIL. 3. IT NEVER FAILS ON THE STATE BUTTONS. Gates on 41-44/57-60/76/89-92 are gestures PLN arms on purpose. They are LISTED ("armed on purpose (not a failure)") so nothing is silently on, but they do not block. A gate that cries wolf about intentional performance state is a gate that gets ignored, and then it is worth nothing on the night it matters. It also names what it cannot see rather than implying completeness: Ardour faders are invisible over MIDI, so the output points at check-mix.py and says that reads the SAVED session. == AND IT IS WIRED IN, WHICH IS THE ACTUAL POINT == gig-up.sh now ends with a readiness gate that runs BOTH preflight and check-mix. Everything above that line only proves processes STARTED; these two are the only steps that ask whether the rig will make a SOUND. Neither aborts the launch — you may be starting up precisely to fix them. This is the "real readiness gates" half of feedback_simple_setup, and it means the surface check happens whether or not anyone remembers to ask for it. Verified: live run reports SAFE with the board as PLN just left it (48 controls touched, gMask correctly detected as retired, ^93 untouched and flagged as sitting at the #55 seed). Negative-tested against a truncated log: exit 2, "recorder is NOT running", no verdict offered. 5 new tests (309 total), including the load-bearing one that a stale log must FAIL rather than pass, and that a COMMENTED-OUT `gMask = id` does not count as retired.PLN (Algolia) authored -
Two changes PLN asked for, plus the five tests that had to be rewritten because they encoded the behaviour we just removed. == 1. gMask IS RETIRED == PLN: "kill gMask! its unprevisible anyway. we can then consider a gMask per track ... tbh gmask is risky id rather invest all on my own manual masking perf" It was `midiOn "^41" (mask "t!7 f")` — top-left button chopping the last eighth out of every bar on every stream wrapped in gM1/gM2/gM3, which in practice is every orbit of every track. A global that silently removes events from everything is exactly what makes a rig feel haunted, and the gesture plays better by hand. It becomes `gMask = id` rather than being deleted: 230 .tidal files name it directly and removing the binding would stop every one of them compiling. Same retirement pattern already used for the midiG family (#73) — keep the name, empty the behaviour, delete the usages at leisure. gM1/gM2/gM3 are now just the mutes. The payoff is the SURFACE, not the sound: ^41 is button-top-row column 1, the one slot the column-aligned button map needs for d1's gate. With it free, the top button row can be fully per-orbit d1..d8 and the whole board reduces to one sentence. That unblocks phase 2 (#92). Worth recording how visible this was: gig-log reported `41 127 gMask someCyclesBy gate -- gates 100% of cycles`. gMask had been fully ARMED on the live board since 15:36 while PLN was testing the set. Nothing was broken; a global was simply switched on and nothing put that in front of him. He has since pushed the state buttons back off. == 2. THE BOARD WAS DENYING d9 EXISTS == PLN, on bombe_dj: "i see no led under d9 while d9 has a sound a FMRhodes". A1-A4 are d9-d12's LEVEL, MIDI-learned in Ardour to the Tidal 09-12 track gains (#46). No .tidal writes ^13-^16 — and it must not, since Tidal never sees those CCs — so a "^NN" scan finds nothing and the knobs went dark. Dark means "not mapped here" (feedback_dark_means_unmapped_outranks_all), so the board asserted that four working controls did nothing. To be precise about a phrasing that confused things: the ORBITS d9-d12 are entirely Tidal's, they carry real sounds (wap's d9 is vec1_acid), and every orbit-parsing tool sees them — HUD, pvlint, silent-eval, surface-columns. What belongs to Ardour is only the four CC NUMBERS. The orbit was always visible; the knob-to-orbit LINK was the missing piece. parse_track now lights A_N exactly when the track DECLARES d(8+N), coloured by that orbit's own family — these are per-orbit by construction, unlike the shared filters and mutes. A track with no d11 still gets a dark A3, because there is nothing there to level. So the row answers the question actually asked mid-set: which extra orbits does this track have, and where is their volume? == 3. FIVE TESTS REWRITTEN, NOT DELETED == They asserted gM1 -> {41,73} and "the mask is measured by DENSITY". Both were true and are now false. Each was retargeted at the new truth rather than removed, and one was ADDED that the old design never needed: test_gMask_is_retired_and_claims_no_cc — asserts ^41 is owned by NOTHING, scanning every helper. A gMask that quietly reclaimed CC 41 would put two jobs on one button, which is the precise failure this remap exists to remove. That deserves a guard, not a comment. Verified: check-boot all 4 passes green (helpers audible against an untouched controller, and the block-seam replay confirms 32 blocks each parse as one statement); silent-eval --seeded on bombe_dj still emits on every declared orbit; 305 tests pass. NOTE the retirement only takes effect on a Tidal REBOOT — the running ghci loaded BootTidal at 16:36, seven minutes before this edit. Until then ^41 still masks.PLN (Algolia) authored -
Two lies the board was telling, both found by PLN looking at it during the column migration. Neither had any error output anywhere; both are the rig's signature shape, a value resolved once and never rechecked. == 1. STALE PARSE: the board showed a file that no longer existed == PLN: "i see the leds red on C5 c6 not on c4 why? feels like old led convention?" His instinct was right about staleness and wrong about the cause — it was old DATA, not old code. The watcher followed track CHANGES only, so it held whatever the file said the moment it was opened. Timestamps settled it in one line: watcher started 15:21:24 bombe_dj.tidal 16:27:17 (the migration) ^54 existed in the 15:21 version and does not exist now. C5+C6 lit / C4 dark was a *faithful* picture of a file 66 minutes dead. And this is livecoding — the file changes constantly, so this was not an edge case, it was every save. follow_loop now stats the track's mtime alongside the path and re-parses on either. Two extra stats per second; still no inotify, deliberately — a watch that dies silently is the exact failure class this rig keeps producing. One subtlety in the fix: touched-state is cleared on a track CHANGE (carrying it over would claim you had already worked controls on a track you just opened) but PRESERVED on an edit to the track already loaded. Otherwise a ctrl+S mid-set wipes the one thing that display exists for. == 2. HELPERS ARMED BY NAME: four live buttons painted as unmapped == PLN: "i expect filters and mutes on all and the ^41 as the mask control no?" Every ParVagues orbit is written `dN $ gF2 $ gM3 $ ...`, and BootTidal defines `gM3 = gMask . gMute3` — so ONE name arms TWO controls, and a "^NN" scan of the .tidal sees neither. On 5 of the 13 OPAL tracks (do_it_right, vague_de_crime, desire, the_revolution_will_be_sampled, electric_hammer) ^41 and all three mutes sat DARK while live. Dark means "not mapped here" (feedback_dark_means_unmapped_outranks_all), so the board was asserting that four of the most-used buttons did nothing. parse_track now resolves gMask / gMute1-3 / gM1-3 to their CCs. Role is the neutral "fx", not the invoking orbit's role: the mutes are SHARED (bombe_dj drives gMute3 from d4, d5 and d7), so an orbit-derived role would be whichever orbit the parser happened to see last. A stable colour beats an arbitrary one; refining button colour is #49/#51. == THE NEAR-MISS WORTH RECORDING == I first "verified" this fix against bombe_dj and measured NO change — 28 bindings before, 28 after — and was about to conclude the whole diagnosis was wrong. bombe_dj is one of the 8 tracks where those CCs happen to appear literally somewhere in the file, so it cannot show the bug at all. Running the comparison across all 13 tracks instead of the one in front of me is what recovered it. A single-file spot-check disproving a real bug is a worse outcome than no check. Tests: 6 new cases in at/tests/test_lcxl_colour.py, one per SHAPE rather than per file — gM3 lights both mask and mute, each gM variant selects only its own mute, bare gMask/gMute names resolve, a COMMENTED helper lights nothing (same rule as a commented ^NN — a disabled helper is not a binding), and a track arming no mask/mute leaves those buttons dark. 42 pass in the colour suite, 165 in the full tools suite.PLN (Algolia) authored -
PLN gave the go and accepted the trade in his own words: "i agree for the trade and change of knobs. i accept the fate! and truthful leds will help quick learn". The faders were remapped this afternoon; doing the tracks now means his hands relearn the surface ONCE, with four days to drill it, instead of learning an interim layout this week and the real one after the gig. 12 tracks, 54 substitutions, 5 gSel FIXMEs, 0 collisions Each of d1-d8 now has its effect on its own B knob (and a second on its C knob for d4-d8), directly above the fader that levels it. bombe_dj landed separately in fdde0924 as the proof track. == WHAT MADE THIS SAFE, AND IT IS NOT THE DIFF SIZE == The migrator computes each track's WHOLE permutation, prints it, then writes once. That is not tidiness, it is correctness: * desire.tidal needs two genuine SWAPS — d7 ^55<->^35 and d8 ^56<->^36. Applied sequentially, the first move would overwrite the second's source and both controls would end up on one knob. Applied simultaneously they just exchange. * the same property dissolved the 34 "destination taken" clashes the naive costing reported: the corpus is uniformly off by one column, so every orbit wants the slot of the orbit below it, free only once that one moves too. Genuine collisions across all 13 tracks: zero. * the rewrite is scoped PER ORBIT. desire had ^19 driving both d7 and d9, which migrate to different destinations; a file-wide replace would have fused two gestures into one, silently. * commented-out ^NN refs are never touched — they are alternatives PLN re-enables mid-set, and rewiring them would break weeks from now with no trace. == THE 5 OVERFLOWS, LEFT IN PLACE ON PURPOSE == perfect d4 ^17, perfect d5 ^18, mafia_sans_serif d7 ^19, the_revolution_will_be_sampled d7 ^55, desire d3 ^52. These orbits have three or more effect knobs; the grid honestly offers two (one for d1-d3, whose C knob is a family DJ filter). Rather than spill them into a neighbouring column and call the board aligned, each carries a FIXME(#54) pointing at gSel. PLN: "if we have two effects, we consolidate via gSel indeed." == VERIFICATION, AND ONE HONEST GAP == pvlint 13 tracks, 0 errors, 9 warnings (all pre-existing: duplicate d5 declarations, one out-of-range sample index — none introduced here) silent-eval --seeded every BUILDABLE track's every declared orbit still emits events, cold, against the seeded control map. This is the check that would catch a renumber having quietly emptied an orbit, and it is clean. surface-columns --knobs 77 knob renumbers -> 5 (exactly the gSel overflows) THE GAP: the silent-eval harness cannot BUILD 3 of the 13 tracks — overlapping IsString instances on chord literals, and an ambiguous `cutoff` between Tidal.Context and Tidal.Params. I re-ran two of them from HEAD and they fail identically there, so this migration did not cause it. But it means three tracks' orbits are UNVERIFIED, and a harness that cannot build a track must never be read as a pass. Filed separately. Also fixed one stale comment of my own in desire.tidal: after the d7 swap the NANANA knob is B7, not C7. A comment that lies about the layout is worse than none when you are relearning the board. Not committed here, deliberately: backlog.md and tools/at/fixtures/claude.tidal carry PLN's own uncommitted edits.PLN (Algolia) authored -
PLN: "lets do the bombe dj first to confirm". One track, end to end, so the tool and the grid are both proven before the other twelve. d3 ^52 C4 -> ^31 B3 legato d4 ^17 A5 -> ^32 B4 midiOff (slow 4) d4 ^53 C5 -> ^52 C4 crushbus 41 d5 ^34 B6 -> ^33 B5 octerbus 52 d5 ^54 C6 -> ^53 C5 crushbus 51 It also cleaned up a collision I had introduced myself twenty minutes earlier: 0538eb97 moved bombe_dj's d9 onto ^17, but d4 was ALREADY using ^17, so between the two commits that one knob drove both orbits. Caught by running --plan before --apply rather than trusting the previous step. The lesson is the tool's design rule, not a footnote: build the whole permutation, then look at it, then write. == THREE DESIGN DECISIONS THAT ARE THE WHOLE TOOL == 1. THE PERMUTATION IS APPLIED SIMULTANEOUSLY. Costed one move at a time, the setlist showed 34 "destination already taken" clashes. Every one was phantom: the corpus is uniformly off by one column, so each orbit's knob wants the slot of the orbit below it, occupied only until THAT one also moves. Build the full map, rewrite in a single pass, and the clashes evaporate. Applying moves sequentially would have corrupted the files. Real collisions across the whole setlist: zero. 2. THE REWRITE IS SCOPED PER ORBIT, never file-wide. Two orbits can legitimately share a CC today — desire.tidal had ^19 driving both d7 and d9 — and they migrate to DIFFERENT destinations. A file-wide search-and-replace would send both to one place and silently fuse two gestures into one. So the substitution walks orbit by orbit with only that orbit's map. 3. COMMENTS ARE NEVER REWRITTEN. A commented-out ^NN is an ALTERNATIVE PLN may re-enable mid-set. Renumbering it would quietly rewire that alternative to a different orbit's knob, and the breakage would surface weeks later with no trace of a cause. Hands off by construction: faders 77-84 and knobs 13-16 (Ardour-learned — the CC77-to-silence footgun), the 8 BootTidal helper CCs, and all buttons (41-44/57-60/73-76/89-92 — gates and mutes are phase 2, and retraining a gate is more disruptive than retraining a knob). == AND A FIX TO THE VERIFICATION LENS, WHICH MATTERED MORE THAN IT LOOKS == After applying, surface-columns still reported "11 moves" for bombe_dj. Nothing was wrong: it counts BUTTON refs too, and buttons are deliberately out of phase-1 scope. But a gate that reports deliberately-deferred work as a failure is a gate you learn to ignore — and that is precisely how a real regression gets through. Added --knobs, which narrows the grid itself so every downstream number speaks about phase 1 only. Same discipline as feedback_parsers_over_copy: the measurement must measure what was actually done. == VERIFIED == pvlint 1 track, 0 errors, 0 warnings silent-eval --seeded every declared orbit still emits events, cold surface-columns --knobs 0 renumbers remaining (was 5) Remaining for bombe_dj, on purpose: d9's second effect (^19) still wants gSel (FIXME already in the file from 0538eb97), and the button rows are phase 2.PLN (Algolia) authored -
First slice of the #92 column migration, and the slice that had live breakage in it. THE GRID, settled with PLN this afternoon after one wrong turn and one measurement that overruled me: A 13-16 d9 d10 d11 d12 LEVEL (Ardour, learned today) A 17-20 d9 d10 d11 d12 EFFECT <- this commit B 29-36 d1..d8 EFFECT (next slice) C 49-51 gF1 gF2 gF3 (unchanged — see below) C 52-56 free, per-orbit 2nd effects D 77-84 d1..d8 LEVEL (Ardour, learned today) The wrong turn was mine: I proposed extending the DJ filters to gF1..gF8, one per column. Then I measured which orbits actually receive each filter across all 13 setlist tracks: gF1 -> d1×13 d2×13 d3×13 d8×12 drums / core rhythm gF2 -> d4×14 bass, almost exclusively gF3 -> d5×12 d7×9 + d9-d12 leads / melodic / extras That is not three arbitrary filters, it is a per-FAMILY design applied consistently across the whole set — you filter a stem group, like a DJ mixer, not one channel. Per-orbit filters would have destroyed it and handed PLN eight knobs he would never use that way. gF1-3 stay exactly as they are, and C4-C8 stay free, which as a bonus removes the "one effect per orbit" constraint I thought the grid forced. PLN's instinct ("i wanna keep C1/2/3 as djf these are core") was righter than his reasoning for it. == THE LIVE BREAKAGE == piment_bresilien.tidal:77 drove its d10 crushbus from "^14". As of today CC 14 is MIDI-learned to Ardour's Tidal 10 gain. So that one knob was crushing the bass and riding the orbit's fader simultaneously — a control doing two unrelated jobs, the kind of thing that reads as "the rig is haunted" mid-set. Now ^18 (A6), which is d10's own effect slot. This was the concrete half of #88. == THE REST == wap d9 ^18 -> ^17 gain swell bombe_dj d9 ^18 -> ^17 midiOff (slow 4) desire d9 ^19 -> ^17 gain swell do_it_right d12 ^31 -> ^20 modIndex (^31 was B3 = d3's slot, and was also Ardour-bound until this afternoon's release) == ONE BEHAVIOUR CHANGE THAT NEEDS PLN'S EAR == In desire.tidal, "^19" drove BOTH d9's gain swell AND d7's NANANA (line 84). Moving d9 to its own knob necessarily splits that gesture. If the link was deliberate — one knob swelling the synth while bringing in the vocal — it is now two knobs and we should restore it. Flagged with a FIXME in the file rather than silently "fixed", because a lost gesture is not something a linter can miss for you. d7's ^19 moved to ^55 (C7, its own 2nd-effect slot) rather than squatting on A7, which belongs to d11. == FIXMEs LEFT ON PURPOSE (PLN: "if we have two effects, we consolidate via gSel indeed. mark FIXMEs in code in there") == wap d9 and bombe_dj d9 each have a SECOND effect (crushbus, modIndex) and d9-d12 get one A-row knob each, so there is no honest home for it. Both marked FIXME(#54) to fold into ^17 via gSel rather than borrowing d11's knob. The wrong fix would have been to spread them onto neighbouring columns and call it done. == VERIFICATION, in the order that matters == pvlint 5 tracks, 0 errors (2 pre-existing warnings, untouched) silent-eval every declared orbit of all 5 tracks still emits events, cold, against the seeded control map — this is the one that proves a renumber did not silently empty an orbit surface-columns Ardour collisions 1 -> 0; the 5 primary d9-d12 controls all land on their own knob; 2 second-effects correctly flagged Uncommitted WIP in 6 setlist tracks was backed up before editing; only CC numbers and comments changed, so PLN's in-progress edits are preserved in the diff. Aside, spotted by pvlint and NOT fixed here: do_it_right d5 references index 8 of "daft" which has 5 samples — that layer never arrives. Own task.PLN (Algolia) authored -
surface-columns reported 11 CCs as "held by BootTidal" — including 18, 34 and 77. All three are false. Every one appears only inside a `--` comment: BootTidal.hs:299 -- midiOn ("^34" - "^18") (perfect.tidal:63) BootTidal.hs:322 -- midiGGlobal used to read the LIVE fader: orDef 0.769 "^77" * 1.3 one documenting a track's idiom, one a note about a retired helper. BootTidal claims none of them. This is the dangerous direction of wrong. An INFLATED helper set says "these controls are unavailable", so it silently shrinks the design space for the #92 column migration — B-row column 6 (^34) and A-row column 6 (^18) would have looked spoken-for when they are free, and ^77 would have looked like a Tidal-side conflict with Ardour's newly-learned fader 1 when there is none. A wrong number that closes doors is worse than one that opens them, because nobody goes looking. Caught it by grepping BootTidal for the three CCs while planning #92 and finding every hit was a comment line. Fix is one line: run strip_comment over the helper block before matching, the same way pvlint and silent-eval already do. Corrected numbers: 8 helper CCs (41, 49, 50, 51, 73, 74, 75, 93) — gMask, the three DJ filters, the three mutes, and panic. Exactly the set you would predict from reading the helpers, which is the tell that it is right this time. Knock-on: the renumber count rises 174 -> 192, because ^34 and ^18 are no longer excused as immovable helpers and now correctly count as track controls that would have to move. #92 updated.PLN (Algolia) authored -
feat(surface): measure whether each orbit's knobs sit ABOVE its fader — the whole corpus is off by one PLN, mid-remap, spotted the half of #46 I had not measured: "but we need the coverage of the effects moving in the tracks, e.g. bass from 81 to 80 means effects on 53 now move to 52" He is right, and my earlier conflict scan answered the wrong question. That scan looked for CC *collisions* — two things claiming one control — and found only two (^78, ^14), which is how #46 came to be costed as "two track edits". But the LCXL is a GRID of eight channel strips, and the property that makes a surface readable is not absence-of-collision, it is COLUMN COHERENCE: the knobs directly above a fader must shape the same orbit that fader levels. Otherwise the bass fader is in column 4 while the bass filter is in column 5, and every reach is a lookup. tools/surface-columns.py measures it: per track, per orbit, which grid columns that orbit's "^NN" references actually land in, and what would have to move for column == orbit. It separates two classes, because conflating them would have produced a work list that is mostly noise: * TRACK controls — a raw ^NN in the .tidal. Free to move; a text edit. * HELPER controls — CCs baked into BootTidal (gF1/2/3 on 49/50/51, gMask 41, gMute1-3 on 73/74/75, gPanic 93, plus 18/34/77 — 11 in all). A track cannot move these by editing itself. They are global by construction and will always read as "misaligned" against a per-column model. THE RESULT, over the 13 OPAL setlist tracks — 84 d1-d8 orbits: only 19/84 orbits are column-aligned; full alignment = 174 ^NN renumbers and the pattern is startlingly uniform across all 13 files: d1 -> col 2 d4 -> col 5 d7 -> col 7 (aligned) d2 -> col 3 d5 -> col 6 d8 -> col 8 (aligned) d3 -> col 4 d6 -> col 7 So the corpus convention is "orbit N lives in column N+1" for d1-d6, and N+0 for d7-d8. The +1 is not an accident: column 1's C-knob and both its buttons are already spoken for by gF1 / gMask / gMute1, so per-orbit controls were pushed one column right to dodge them. And the two rules meet badly — column 7 is double booked by d6 and d7 (in desire.tidal both really do react to ^59). The consequence for #46 is the useful part: because the corpus is +1 for six orbits and +0 for two, THERE IS NO FADER MAPPING THAT MAKES TODAY'S TRACKS COHERENT. Shifting the faders +1 to match would strand d8; leaving them arbitrary is where we are. Either the tracks move, or the surface stays a lookup. PLN's instinct — that the Ardour re-learn is only half the job — was exactly right. Survey, not a gate: exits 0 always, because alignment is a design choice and this tool's job is to price it, not to enforce it. --plan prints the exact ^NN -> ^NN moves per track for when we do it (#92).PLN (Algolia) authored -
The board's colours only persist because a daemon holds a model of every control and repaints from it. That daemon had no home. It could be started three ways: 1. by hand, tools/lcxl-leds.py --watch 2. by gig-up.sh, which `setsid`-spawned its own copy (GIG_LEDS=watch) 3. as a *transient* systemd unit, which is how it was actually running today (systemd-run --unit=lcxl-leds-watch) Every one of those is wrong in a different way. (1) dies with the terminal. (2) does not know about (3), so launching gig-up on a machine that already had the watcher up gave you TWO processes writing SysEx to the same LCXL, fighting over every LED — and neither of them wrong enough to look broken, which is the worst kind of bug this rig produces. (3) has no file on disk, so it evaporates at the next reboot and the board silently stops persisting colours. PLN, on being shown the three: "watcher must be a saved tool part of gear indeed". So: tools/lcxl-leds-watch.service, symlinked into ~/.config/systemd/user/ the same way parvagues-bridge.service already is, enabled, WantedBy=default.target — it starts at boot with linger, before any login. gig-up.sh no longer spawns anything; it `systemctl --user restart`s the unit, which is idempotent AND guarantees exactly one owner even if a stale watcher survived a crash. One owner of the board, always. Two details worth the ink: - StartLimitIntervalSec=0 belongs in [Unit], not [Service]. Put in [Service] systemd says "Unknown key ... ignoring" — a warning in the journal nobody reads — and the default limit of 5 restarts in 10 s stays in force. The LCXL is hot-pluggable and usually absent at boot, so with Restart=always/RestartSec=10 the unit would burn its five retries and fall into `failed`, board dark for the rest of the session. A silent failure one section heading away from working. Caught it because the first install DID log the warning; fixed and re-verified with systemd-analyze. - Cost, measured from the transient unit's own accounting before replacing it: 2.140 s CPU over 1 h 53 m wall = 0.03% of a core, 14.5 M peak RSS. The watcher forks a helper per LED write, which is a real throughput problem for the 1-2 s paint lag — but it is emphatically not a load problem, so it can stay Nice=5 / CPUWeight=20 and never be a candidate when hunting xruns. Verified: unit enabled + active, systemd-analyze verify clean, no Unknown-key warning on reload, `bash -n gig-up.sh` clean, and exactly one watcher process owned by the unit (MainPID matches, NRestarts=0). Note `pgrep -cf 'lcxl-leds.py --watch'` reports 2 — it counts the shell running the pgrep pipeline itself. Read the process list, not the count. Closes #85.PLN (Algolia) authored -
PLN (Algolia) authored
-
docs(tasks): archive #79/#82/#78/#8/#61/#81 — the seed that never ran, and a task built on a guessed unit name
PLN (Algolia) authored -
PLN, after the value ramp landed: "now all knobs have lights, i cant trust anymore 'is there sth mapped there or not?' knobs should always be no-lit if they are not-mapped, in that track, to help not touch dead controls". The interesting part is that the colour code was never wrong. build_frame() already paints only the CCs present in `bindings` and leaves everything else OFF. The lie was in WHICH BINDINGS IT WAS HANDED: the watcher runs with no track argument, so load_bindings(None) returns the CONVENTION board — all 40 controls lit by lane role, regardless of what is actually loaded. That was equally untrue before the ramp; dim role-hue just made it easy to ignore, and saturated colour made the board finally READ as "everything here is live". So the feature did not create the problem, it made a pre-existing lie legible — and fixing the colour back would have hidden the fault again. MEASURED: convention board (what the watcher painted) 40/40 lit do_it_right 19/40 -> 21 dead knobs lit wap 25/40 -> 15 desire 22/40 -> 18 Fifteen to twenty-one controls were glowing on every track while doing nothing. On a dark stage that is a knob you reach for and a change you do not get. The fix is a published current-track file (~/.cache/parvagues/current-track): * `--map TRACK` now PUBLISHES as well as paints. Without that, a one-shot paint at boot showed the right board for 30 seconds and then the watcher's re-assert overwrote it with the convention paint — the same board, lying again. One writer, one meaning. * `--watch` with no pinned track follows the file on a 1s poll and repaints on change. Polled rather than inotify on purpose: a watch that dies unnoticed is exactly this rig's signature failure (a binding resolved once, never rechecked), and one stat/second of a small file costs nothing measurable. * touched-state clears on track change (carrying it would claim you had already worked controls on a track you just opened); VALUES persist, because the knobs did not physically move. * bindings moved into a shared box — three reads in the aseqdump loop and the re-assert thread would otherwise have kept painting the previous track's map. load_bindings(None) now also SAYS it is painting a board that may be lying, instead of reporting "40 controls lit by lane role" as though that were good news. The remaining gap, deliberately not closed here: nothing publishes the track when PLN ctrl+enters a file in Pulsar — only `tidal-remote boot` does. The clean signal is a five-line hook in the HUD package, which already tracks the active .tidal. Filed rather than rushed the day before rehearsals. 458 tests (was 453). NOTE: the running watcher must be RESTARTED to pick this up; not done now, because PLN is about to play and a dark board mid-set beats a correct board that arrived by surprise.PLN (Algolia) authored -
BootTidal.hs:376-377 applies BOTH: gDJF ch = (# lpf (range 180 20000 (fmap (\v -> 1 - 2 * max 0 (0.5 - v)) ...))) . (# hpf (range 20 8000 (fmap (\v -> 2 * max 0 (v - 0.5)) ...))) Yesterday's `left at` column transcribed only the `# lpf` line, so it called gF3-parked-at-80 "open" when it is really a ~2 kHz HIGH-PASS — which guts a bass or a voice, and which is exactly the helper PLN had commented off two different d5 orbits to get the sound back. The report was confidently wrong about the whole upper half of the knob, in the direction of reassurance. Now models both bands and grades on the pair: 0 lpf 180 hpf 20⚠ ⚠ NEAR-SILENT 64 lpf 20000 hpf 83 open 80 lpf 20000 hpf 2094⚠ thin — low end cut 127 lpf 20000 hpf 8000⚠ ⚠ NO BODY LEFT Hard right is not "open". It never was. Two of the existing tests encoded the old blind spot — they used cc 49 = 100 as the "safe" control value, which is hpf 4607 Hz. The code was right and the tests were wrong, so the tests moved to the centre. And the new test caught something small and real: "centre = true bypass" is an APPROXIMATION, not an identity. 0..127 is an ODD range, so 0.5 falls between cc 63 and cc 64 and no cc value hits bypass exactly — 63 gives a ~19.7 kHz lowpass, 64 an 83 Hz highpass. Both inaudible, so the knob is fine in practice, but the test now asserts the truth rather than the comment in BootTidal.hs. 453 tests green across tools/.PLN (Algolia) authored -
The report ended an unclosed log with:⚠ no `end` record — the recorder is still running, or it was killed Both readings are plausible and the reader has no way to pick. I picked wrong: I asked systemd about `parvagues-gig-log` — a name I guessed instead of read; the unit is `gig-log.service` — got "inactive", and used that false negative to resolve the ambiguity into "killed". The recorder had in fact been up for eleven hours, enabled, with its pw-top and aseqdump children alive, still writing. A whole task got filed about restoring a service that was never down. Two mistakes worth naming because they chain: guessing an identifier rather than reading it, and then letting a broken check settle a question the tool had deliberately left open. The wording invited exactly that. So the tool now decides and says which: ● RECORDING NOW — this log is still open, numbers are partial⚠ no `end` record and no recent sample: the recorder was KILLED is_live() decides from the DATA's own recency — last sample within a few periods of now — and deliberately not from a process match. `pgrep -f` matches any shell that merely mentions the string, and unit names are exactly the thing I just got wrong. Recency needs no name and nothing to guess. 86 tests (was 83): a live log must say RECORDING NOW and never KILLED, a stale one the reverse, and is_live must answer from timestamps alone.PLN (Algolia) authored -
PLN this morning: "moving to track 2, wap.. no bass? when i move the knob C5 it starts sounding". And on do_it_right: "ctrl_enter, i hear the 4-bar pattern each bar lower, 4th barely audible, its clearly a xfade". Both sentences are the same bug, and the second one hid the first for days. THE MASK. Every dN is `xfade N` with xfadeIn 4, so the four bars you hear after an eval are the PREVIOUS pattern leaving. A silent eval gets a graceful exit and reads as a "drift to silence". The sound after ctrl+enter is not evidence the eval worked — it is evidence the last one did. THE BUG. Pulsar does not feed BootTidal.hs to ghci verbatim. boot-tidal.js splits it on BLANK LINES and strips the `:{`/`:}` it finds; repl.js tidalSendExpression then wraps each chunk in its OWN `:{ ... :}`. A ghci `:{ ... :}` accepts exactly ONE statement. The #55 seed was written as :{ let _seed = concat [ ... ] :} mapM_ (\(k, v) -> setF k (pure v)) _seed putStrLn "[BootTidal] seeded ..." — correct for a file read line-by-line, and fused by Pulsar into a single statement that dies with "parse error (possibly incorrect indentation or mismatched brackets)". So THE SEED NEVER RAN. Not once between 2026-07-27, when it was written, and today. Every boot left the control map empty. An untouched "^NN" yields NO EVENTS — not 0, nothing — so any `# param (range a b "^NN")` emptied its whole orbit, and the rig only made those sounds after a knob was physically moved. Which is precisely what PLN described, in the sentence I had filed as a separate question about crush ranges. MEASURED, with tools/silent-eval.py (added earlier today) over the OPAL setlist: empty control map 23 orbits silent across 10 of 10 buildable tracks, including desire d1-d6 — the ENTIRE track with the seed ZERO silent. Every declared orbit emits events. The seed is necessary AND sufficient. The tracks were never the problem. THE FIX is two blank lines. They are load-bearing and the file now says so. Verified through the real seam, not by inspection: replicating Pulsar's exact chunking and wrapping and feeding it to ghci now yields 51 setF calls with the right values (13=0.0, 49=0.5, 77=0.769, 78=1.0 ...) and prints the boot banner. Before the fix the same harness produced only the parse error. WHY EVERY EXISTING GUARD MISSED IT. check-boot.sh passes 1-3 prove the helper block typechecks, the seed block typechecks, and the helpers emit events against an empty control map. All three were green throughout. They test the Haskell; the failure was in how the file is CHUNKED AND FED. Green checks on a component say nothing about whether the data reaches it — the rig's signature failure, and this is the purest instance of it yet. #61 was closed on exactly that false comfort. So this adds check-boot.sh pass 4 / tools/check-boot-blocks.py, which replicates Pulsar's chunking bug-for-bug (the non-global .replace included), feeds every block to a bare ghci, and fails on parse errors only — "not in scope" and type errors are expected without Tidal and are ignored. Negative-tested against the pre-fix file: it names block 26 at BootTidal.hs:663 and exits 1. One bug found in the guard before trusting it: capturing stdout and stderr separately and concatenating them put every marker before every error, so each parse error was attributed to the LAST block rather than its own — it confidently blamed block 28. Now one interleaved stream.PLN (Algolia) authored -
PLN, playing this morning: "ctrl_enter on do it right, i hear the 4-bar pattern each bar lower, 4th barely audible, its clearly a xfade". Every dN is `xfade N` with xfadeIn 4, so those four bars are the PREVIOUS pattern leaving — the new one was already silent, and the crossfade was handing the bug a graceful exit. Two guards existed and neither could see it: * check-boot.sh runs the g* HELPERS against an empty control map — but the dangerous "^NN" uses are in the TRACKS. `# crushbus 41 (range 16 4.5 "^53")` is do_it_right's line, not BootTidal's, so check-boot stayed green while that orbit emitted nothing. * check-tracks.sh measures real audio — ground truth, but it needs the rig, a quiet house and ~45s a track. This is the missing middle: rewrite each `dN $ ...` into a plain binding on top of BootTidal's dedented helper block (with the track's own `let`s shadowing, as they do live), then QUERY it with a deliberately empty controls map. Pure pattern evaluation — no stream, no scsynth, no MIDI, no port 6010 — so it is safe to run mid-set and answers all 13 tracks without anyone's ears. RESULT, and it is unambiguous: empty control map 23 orbits silent across 10 of 10 buildable tracks, including desire d1-d6, i.e. the ENTIRE track with the #55 seed ZERO. Every declared orbit emits events. The seed is necessary AND sufficient. The tracks are not broken; the silence PLN heard means the seed did not reach the running Tidal. TWO BUGS FOUND IN THIS TOOL BEFORE TRUSTING IT — both would have been confidently wrong findings, and both were caught by looking at output rather than at code: 1. The seed parser used `[^\]]+?` to capture `n <- [13..20] ++ [29..36]`, which stops at the first `]`. It silently produced 4 seeds instead of 51, so the "seeded" run was indistinguishable from the unseeded one. Now line-based. 2. Far worse: the first version queried ONE cycle and reported six orbits as silent. Every one was a false positive — `mask "<f!24 t!8>"`, `"<~ [~ ~ ~ cheval]>"`, `n "<~ <~ 7> ~ 5>"` are alternations whose cycle 0 is empty BY DESIGN, risers that fire once every 8 or 32 bars. Collapsing the time axis cannot distinguish sparse from dead. Now queries a 64-cycle window and prints the first sounding cycle when it is later than 4. Honest limits: 3 of 13 tracks (perfect, mafia_sans_serif, the_revolution) do not yet build in the harness — `cutoff` is ambiguous between BootTidal and Sound.Tidal.Params, and one chord literal needs its type pinned. These are reported as BUILD FAILED and explicitly never as a verdict about the music, so a harness limit cannot masquerade as a finding.PLN (Algolia) authored -
The surface table said cc 51 moved 533 times somewhere between 1 and 127. That is a biography, not a state, and it cannot answer the only question you ask a log at 3am: "why is that orbit silent?". Only the LAST value can. The data was already in the file — MidiReader.feed() has always coalesced to count/first/last/min/max per control per second, so `v1` is the value the knob was left at. The report simply never printed it. This is a formatting change to capture that already happened. Three additions: * `report` gains a `left at` column, and for the three DJ filters it prints what that value MEANS in hertz. Transcribing BootTidal.hs:376 turned up something worth its own note: for v <= 0.5 the gDJF expression reduces to lpf = 180 + 39640*v, i.e. LINEAR IN HERTZ. Pitch perception is logarithmic, so that knob spends nearly all its travel in the top two octaves and crosses the entire audible bottom in the last ~2% — it feels inert, then collapses. That is one gesture producing both "it went quiet" and "it sounds lpf'd", which is exactly the pair of symptoms #79 was filed with. * `gig-log.py controls` — the whole surface, by CC, with each value translated: filters to Hz, gMute/gMask to "mutes N% of cycles", panic to armed/clear. It ends with an explicit list of anything parked somewhere that silences or thins the sound, because a table you have to interpret under stage lights is a table you will misread. * `report --from/--to`, wall clock or +M:SS. The recorder ran 10h48m of which ~90 minutes was playing and eight hours was tooling; every aggregate therefore described the wrong thing. Windowing has one non-obvious requirement, and it gets a test of its own: xrun and throttle are RUNNING TOTALS, so a naive slice reports the whole session's count inside the window — wrong, and wrong high. slice_session() rebases them to the window. VALIDATION — it immediately paid for itself by killing two hypotheses: 49 51 gF1 lpf 16098 Hz — open 50 59 gF2 lpf 18595 Hz — open 51 80 gF3 lpf 20000 Hz — open 41 0 gMask gates 0% of cycles 73/74/75 0 mutes 0% of cycles #79's two leading suspects were "a DJ filter parked below centre" and "a mask left engaged". Both are now dead, from a file, with no rig and nobody's ears. 83 tests (was 71). The new ones pin the BootTidal arithmetic so it cannot drift from the Haskell, the last-value ordering rule (latest TIMESTAMP wins, not file order), the cumulative rebase, and midnight-crossing wall-clock parsing.PLN (Algolia) authored -
PLN (Algolia) authored
-
PLN (Algolia) authored
-
check-tracks.sh is THE empirical gate: it boots every track in the set and proves each declared orbit makes sound, which is also the only compile check a .tidal file can have (there is no static typechecker for a GHCi fragment). The comment at the top says to run it the day before the gig. It read `setlist_opal2026.txt` from the repo ROOT. That file has never existed. So the gate has been unrunnable since it was written — and it failed in the least helpful way possible, printing "no such file" per track from an empty list rather than saying the setlist was missing. Now points at `armada/setlist_opal2026.txt`, the file added for #68, so ONE list drives both questions: "does every orbit sound" (this) and "which orbits ghost across each transition" (orphan-orbits.py). Reorder the gig, re-answer both. Two fixes it needed to actually read that file: * strip TRAILING comments, not just full-line ones — the setlist annotates each path with its codename and BPM (`live/…/wap.tidal # WAP [133]`), and leaving that on the line makes every track report "no such file"; * fail loudly when the setlist is absent, instead of silently iterating nothing. Also: `--help` was taken as a track name, so asking for help answered "FAIL — no such file", which reads like the rig is broken. Verified: all 13 tracks resolve through the shell reader. Pinned by two tests — one asserts check-tracks.sh and orphan-orbits.py name the SAME setlist file (a broken gate is invisible until the day you need it), the other runs the actual sed pipeline and asserts no comment text leaks into a path. Suite 435 -> 437, all green. NOT run end-to-end: it needs audio, and it is ~45 s per track by design (a tight loop lands evals on a half-loaded interpreter and reports false failures). It is the natural companion to tomorrow's run-through.PLN (Algolia) authored -
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.PLN (Algolia) authored -
PLN: "i rightclick the perf indicator and see no gear status?" He was right, and the state had been there all along. launchers.is_running() existed; _build_menu() just ran ONCE at startup and read only `available`. So a running Ardour looked identical to a stopped one, and — worse — the safe action ("do nothing, it's already up") looked identical to the dangerous one. Two SuperColliders is a zombie port 6010 and a silent rig. Now: a `Gear ▸ 3/6 up` submenu, each row labelled with what it IS — ● Pulsar — running (greyed: launching a second one is never what you meant) ○ MIDI Monitor (clickable) ✗ Something — not installed Glyphs rather than colour, because a tray menu inherits the desktop palette and this gets read in a dark room seconds before playing. Web tools stay clickable when running, since clicking them opens a URL rather than spawning a duplicate. Refreshed on menu-OPEN only, never on the 2 s icon timer: reading gear state walks /proc, and doing that 30x a minute for a label nobody is looking at is exactly the per-tick cost this rig keeps getting bitten by. THREE REAL BUGS FOUND WHILE WIRING IT UP, each worse than the missing label 1. THE TRAY OPENED THE WRONG ARDOUR SESSION. ARDOUR_SESSION pointed at "Tidal Multi" — the older ARCHIVE of per-orbit recordings — not "Tidal Live", the session that performs and records the stems. Any faders touched there would have been the wrong ones. Auditing that same archive as if it were live already produced a confidently wrong fader report on 2026-07-28; this was the same mixup one layer down, waiting to happen again 6 days before OPAL. 2. IT COULD NEVER HAVE LAUNCHED ARDOUR ANYWAY. Candidates were ardour8/7/6/ardour; the installed binary is ardour9 (real exe `ardour-9.2.0`). So the entry reported "unavailable" and greyed itself out while Ardour was running on the same machine. 3. THE RUNNING CHECK MATCHED THE WHOLE WORLD. `pgrep -f ardour` matched the `tidal-ardour-autoroute.sh` helper script AND any shell whose command line merely mentioned the word — including the shell I was testing from. So it could report Ardour UP while Ardour was DOWN, which is worse than reporting nothing, because it is the state you act on. is_running now matches the EXECUTABLE basename (`exe`), with full-line matching kept only for interpreted tools where argv[0] is `python3` and the identity is the script path. AND IT NO LONGER FORKS is_running was one `pgrep` subprocess PER launcher, and snapshot() is called by the web Bridge's poll as well as the tray. Replaced with a single forkless /proc scan shared across the whole snapshot: 6 items in 10.7 ms, zero forks, down from 6 forks per refresh. Same lesson as the LED daemon's per-event fork and gig-log's per-sample sampling — on an audio rig, do not pay a process for a boolean. Verified live: all six entries now report correctly (Pulsar/Ardour/QjackCtl up, MIDI Monitor + Foundry + Armada down); perf-tray restarted and active. TESTS: +11 in test_launchers.py, suite 424 -> 435, all green. Pinned: the autoroute-script and bare-shell false positives; a version-suffixed binary (ardour-9.2.0, and a hypothetical ardour-10.0.1) matching; a filename ending in ".ardour" NOT counting as a running Ardour; snapshot() scanning /proc exactly once; is_running spawning no subprocess at all (subprocess.run/Popen monkeypatched to raise); and the session path being the LIVE one with "Tidal Multi" absent.PLN (Algolia) authored -
PLN's ask, verbatim: "all controls have impact on sound... even the crushes should change noticeably". The existing suite proves the RIG works against a fixture; this proves a TRACK is wired, which is the thing that bites at 160 BPM when a knob turns out to do nothing. python3 tools/at --track live/.../gimme_acid.tidal --dry-run # no audio python3 tools/at --track live/.../gimme_acid.tidal [--json] Per control: park at REST, take TWO baselines, probe, restore, judge. Four outcomes, not two — MOVED / NO_IMPACT / INCONCLUSIVE / UNMEASURABLE — because a swing that does not clearly beat the pattern's own drift is genuinely unknown, and calling that a pass is how a green suite comes to mean nothing. NO_SIGNAL is kept separate from NO_IMPACT: "nothing was playing" and "the control is dead" have different fixes, and conflating them once cost a whole evening chasing SuperDirt. THE BUG THE FIRST DRY RUN FOUND, which is the real content of this commit The plan for gimme_acid came back "0 bipolar, no CC 49/50/51 at all" — on a track that applies gF1/gF2/gF3 to SIX orbits. gF1/gF2/gF3 are defined in BootTidal.hs, not in the track, so lens.py's scan of the .tidal file alone was structurally blind to the three DJ FILTERS and to EVERY MUTE in the rig. The controls PLN reaches for most were the ones the acceptance test could not see. Fixed by PARSING BootTidal.hs (parsers-over-copy — it is the source of truth and it moves: gF1 was rewired onto gDJF, midiGGlobal was retired hours ago, and a hardcoded table would rot into a confidently wrong report). boot_helpers() now resolves: * `gF1 = gDJF "^49"` -> CC49, and BORROWS its lens from gDJF's body, since "gDJF" is not a keyword any table knows and the lpf/hpf evidence sits on two separate lines * `gM1 = gMask . gMute1` -> composition, inheriting CC41 AND CC73 * a track's own `let gMute = ...` SHADOWS the boot one, as Tidal does Result on gimme_acid: 18 controls -> 22, with 49/50/51 present, bipolar, resting at 64 and probing downward. Across the OPAL set: 275 controls, 15-29 per track, all three filters resolved in all 13, zero Ardour-owned CCs. Two parse bugs found and fixed on the way, both by reading the output instead of trusting it: * `mask "f*16"` was classified as DENSITY. An all-false mask is a MUTE — same keyword, opposite lens (rms vs onsets) — and my first all-false test asked "does it contain a 1", which the *16 repeat count satisfies. So gMute1/2/3, three textually IDENTICAL helpers, came back mute/density/density: rms would have been the wrong lens on two of PLN's three mutes, and two working mutes would have reported NO_IMPACT. * `gM3 = gMask . gMute3` swallowed the Launchpad block that follows it and reported CC 7 and CC 9 as mutes, because continuation lines were appended to whatever was defined last. A continuation now has to LOOK like one. SAFETY CC93 is added to a new lens.NEVER_PROBE. It arms panic — gPanic gates on "^93", so sending it high silences every gPanic'd stream. "Probe every control" plus "one control is a kill switch" is how a self-test mutes a rehearsal and gets blamed on the rig. It is reported and never swept. CC77-84 stay excluded by construction in lens AND re-asserted at the probe; CC77 down is total silence, so belt and braces is proportionate there. Controls are restored per control, not at exit, so Ctrl-C mid-run is safe. Restore goes to Control.rest — 64 for a DJ filter, because djf 0.05 is a ~26 Hz low-pass, i.e. silence, and parking one at 0 to get a "baseline" measures silence and then calls the track broken (#48). --dry-run exists deliberately: it prints the plan with no audio and no MIDI, so the half that can be checked tonight — which controls were found, which lens each gets, what will be sent, what will be restored — is checkable at all. "Built but never run" is exactly how the LED work shipped a colour ramp that was never called. TESTS: +79 (22 in test_lens.py, 57 in the new test_track_runner.py), suite 345 -> 424, all green. Every boot-helper regression above is pinned, including a parametrised all-false-mask table with `f*16` in it; each lens is asserted to have a floor (2 * max(drift, 0) is zero, so without one any tiny swing looks significant on a quiet capture); the probe is asserted to REFUSE all of CC77-84; and the dry-run plan is built for all 13 OPAL tracks with the DJ filters asserted bipolar-and-resting-at-64 in each. STATUS: the judgement half is validated. The AUDIO half is built but unrun — it needs monitors, and the house is quiet. First thing to run with sound up.PLN (Algolia) authored
-
- 28 Jul, 2026 16 commits
-
-
PLN, by ear, 2026-07-28: loaded vague_de_crime, played bombe_dj, and "heard diam's voice, now still hearing the synth from crimewave... i hear it regardless of gains, but filters work on them". THE MECHANISM, and why his two symptoms are the proof `dN $ ...` replaces orbit N and says NOTHING about the others, so every orbit the outgoing track declares and the incoming one does not just keeps running forever underneath the new track. "Regardless of gains" follows because the new track's gain lines address only ITS OWN orbits — nothing in it can reach the ghost. "But filters work on them" follows because gF3 is the GLOBAL CC51 DJ filter, applied INSIDE the ghost's own still-running pattern. An orbit that ignores the faders but answers the DJ filter is diagnostic of exactly this, and nothing else. Not a rig fault. It bites at every transition where the outgoing track declares an orbit the incoming one does not — which is 8 of the 12 OPAL transitions. WHAT SHIPPED armada/setlist_opal2026.txt — the set as DATA, in play order, resolved from backlog.md's codenames by locating each file rather than guessing ("Mafia" is mafia_sans_serif, "Le shifteur marteau" is electric_hammer, "Take five Drops" is take_5_drops). Reordering this file re-answers the ghost question, which is exactly what #12 needs. tools/orphan-orbits.py — reuses pvlint's existing orbit parser (DRY) and: * walks the set pairwise and names each transition's ghosts WITH THE SOUND they play, because "d6 (crimewave)" is actionable on stage and "d6" is not * --matrix, the full A->B grid, so #12 can order the set with the ghosts in view * --silence TRACK, a paste-ready `dN $ silence` preamble * --pair A B, exiting non-zero when ghosts exist so #44 can gate on it THE RESULT FOR THIS SET 12 transitions, 8 leave ghosts, 16 orphaned orbits. Worst: after REVOLUTION (4), after Gimme Acid (3). The original ear-report reproduces exactly — vague_de_crime -> bombe_dj leaves {6, 10}, and d6 IS the crimewave synth. And one ordering lever worth more than the report: Perfect <3 declares ALL TWELVE orbits, so its column in the matrix is entirely clean. It is a free RESET POINT — put it after the messiest stretch and ghosts stop propagating. The tool now detects and names reset points generally rather than leaving that to be noticed. WHY IT STOPS SHORT OF FIXING IT AUTOMATICALLY The obvious automation is to have tidal-remote eval a generated silence block. Read the plugin first: `eval-file` calls atom.workspace.open(..., activatePane: true) — it OPENS AND ACTIVATES the file, so it would yank the editor away from the track being played, mid-transition. That is worse than the ghost. Until the plugin gets a focus-free `code` command, the fix is `hush` (safe under pressure, costs the tail) or pasting the block at the top of a track — and that is a .tidal edit, which is PLN's call, not mine. `dN` IS `xfade N`, so `d6 $ silence` should FADE the ghost rather than cut it; that wants an ear before it is assumed. TESTS: 39 new, suite 306 -> 345, all green. The one that matters is the cross-validation: the parser's orbit set is asserted against the TEN sets hand-measured on 2026-07-28, before this parser existed. All ten agree, so the tool and the ear agree. Also pinned: a commented-out `-- d6` counts as neither declared nor overwriting (correct in both directions); the silence block never silences the track's own orbits (that shape is a mute-bomb and would be blamed on the rig); it uses `silence` and never `# gain 0` (which leaves the pattern scheduled and revivable by a later global gain); and d1-d5 never orphan anywhere in this set — so if a future track drops one, the ghost lands on the loudest possible orbit and the suite says so before the stage does.PLN (Algolia) authored -
Two colour bugs PLN found BY EYE, on the hardware, in the same glance. Both were cases of the code being confidently reasonable and visibly wrong. #76 — "why are [mutes] 1/2/3 resp red red green? these 3 buttons have same roles, why diff colours?" He was right and the cause was in two places at once. The panic feature is an OVERLOAD: hold LCXL buttons 73+74+91+92 together, the SC bridge edge-detects the chord and flips a persistent "^93" toggle, and gPanic gates on it. But those buttons have day jobs — 73 is gMute1, 74 is gMute2 — so: * control_colour tested `cc in PANIC_CHORD` BEFORE role, so 73/74 painted dim RED at rest while 75 (gMute3, identical job, not a chord member) painted by role. Three buttons, one job, two colours. * parse_track ALSO force-bound all four members to role "fx" (red hue) via setdefault, so even on a track binding gMute3 and not gMute1/2 the row read literally "R R G". Found by rendering mock-lcxl.py after fixing the first half and noticing the row still looked wrong. The panic identity does not exist at rest, so it must not be painted at rest. Now: `panic` is threaded in from the model (`values[93]`), the chord members paint by role like any other button when it is clear, and arming flashes red on all four as a GLOBAL OVERLAY applied last — over role, and over dark, because an unbound member must still flash. CC93 owns no LED of its own (it sits 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: it answers "why is there no sound?". The watch loop rebuilds the whole frame on a ^93 flip rather than painting one index, since one event changes four LEDs. Verified by rendering the convention board: row F at rest is now `dimGRN dimGRN dimGRN dimAMB dimAMB dimAMB dimGRN dimAMB` — identical to row E, which is exactly what PLN reported seeing on row E ("G G G Y Y Y G Y"). Armed: `RED! RED! grn amb amb amb RED! RED!`. #11a — the DJ filter is THREE states now, not seven Settled by PLN: "sunset the dimorange and just have red in lows green in highs for clarity" + "i agree on clarity > resolution". The old ramp spent four of its seven steps on dim-amber and mid shades either side of centre, so the row read as a wash of oranges at a glance and the one thing you actually need — WHICH WAY is this filter cutting — was the hardest bit to see. Now: bright red (LPF, lows) / bright amber at the 61-67 detent band (bypass) / bright green (HPF, highs). All three are palette corners, the only states that read reliably on a dark stage. The centre band stays bright because djf 0.5 / CC 64 is BYPASS, not zero — djf 0.05 is a ~26 Hz low-pass, i.e. silence, which is the #48 footgun this colour exists to keep visible. #11b — "I See only two states, dim and not dim, at 0 and not 0 atm on the knobs" Also true, and embarrassing: `value_ramp` — his own verbatim six-step spec — was written, unit-tested, documented in the module header, and never once called. control_colour had exactly three outcomes for knobs (dim / full / flash), so a knob at 30% and one at 90% were the same colour. Wired in. The trade, stated because it should not regress silently: the ramp spends all three hues on VALUE, so hue no longer carries ROLE on rows A/B/C. That is the right way round — role is already fixed by POSITION (the lane convention), while a knob's setting has no other channel at all, since the pot's pointer is invisible on a dark stage. Keeping "untouched" as a colour was tried and abandoned: dim green would have meant both "rhythm, untouched" and "value ~3/4". That nuance belongs in the HUD, which has unlimited colours. Unbound controls still go DARK, which is the distinction that actually matters. Module header rewritten to describe the convention that now exists, rather than the one it used to. TESTS: +24 in test_lcxl_colour.py (8 -> 32), suite 282 -> 306, all green. The old knob rule had NO test at all — which is how a never-called ramp survived being shipped. Now covered: the filter has exactly three states and no dim amber survives; the detent band is contiguous and symmetric; the three mutes are equal at rest AND when engaged; arming repaints all four members and nothing else; an omitted panic argument behaves as unarmed (the #55 "untouched means zero" lesson); the overlay beats both role and dark; and the chord is asserted NOT to be force-bound as fx. Live: lcxl-leds-watch restarted, board repainted.PLN (Algolia) authored -
After a set, "did it glitch?" and "did it get hot?" and "which controls did I actually use?" are answered from memory. Memory is a bad instrument, and the answers matter: #8 (thermal impact under load), #56 (Pulsar starving audio), #46/#11 (which surface controls are really in the hands). PLN records the run-through in Ardour; this gives that take a machine-readable twin. WHAT IT DOES `gig-log.py record` 1 Hz JSONL: package/core temp, fan, freq, throttle counters, per-core cpu%, and per-gear %cpu + RSS for scsynth / sclang / ArdourGUI / the whole pulsar process group. Plus PipeWire xruns and every LCXL control move. `gig-log.py mark ...` annotate the live session ("gimme acid drop") `gig-log.py report` render it back: sparklines, xrun timeline, gear table, a per-CC surface table with first/last touch `gig-log.py status` is it running, what is it writing `gig-log.py install` systemd --user unit, enabled, starts with the session `gig-log.py selftest` prove the parsers AND the cost Wall-clock stamped in the header, so the timeline lines up with the Ardour take. THE OBSERVER MUST NOT PERTURB — measured, not asserted This rig has twice produced the fault it was measuring: probe-chain's capture streams caused the xruns it was hunting, and the LED daemon's per-event fork caused the lag it was reporting. So: no audio capture at all, no per-sample subprocess spawn (every number comes from sysfs/procfs), and exactly two long-lived children (`pw-top -b`, `aseqdump`) drained by threads so a filling pipe can never block them. Measured cost of the whole thing: 0.40% of one core with 270 pw-top lines parsed in 10 s; 0.79% as the live systemd unit. The first selftest reported 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; fixed, then re-measured lower and honest. AND THE LOGGER MUST NOT BE A FIREHOSE A fader sweep is 100+ events/s. A logger that writes them all costs more than what it measures, so CC/pitchbend are coalesced to one line per control per second carrying count + first/last/min/max — the #71/#72 fix applied to ourselves. 128 events on one knob is one line that still shows the whole travel. NOTES ARE NEVER COALESCED: a CC is a state and may be superseded, a note is an event and dropping one loses a thing that happened. COUNTERS ARE DELTAS, AND THE BASELINE IS WHERE THE BUGS LIVED Two absolute counters here are large and meaningless alone: package_throttle was 17024 after 2 idle days, and Ardour's PipeWire ERR was 67072. Both count history, including power excursions and device changes that never touched audio. So the header records baselines and samples record deltas. Getting Ardour's baseline right took three rules, and the first report caught each one: 1. baseline on FIRST sight -> pw-top prints a zero-filled snapshot before the profiler has data, so base=0 and a 25-second IDLE session reported 72096 xruns. 2. baseline on SECOND sight -> usually right. pw-top emits a non-deterministic NUMBER of zero tables, so it silently reported 67123 on the next real run. 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. 3. baseline = MAX over each node's first 5 sightings. ERR is monotonic within a node's lifetime, so the max over a warm-up IS the true starting count — no timing assumption at all. A value below the baseline means the node was destroyed and recreated, so re-baseline instead of reporting negative. Validated by three independent 15 s runs, all reporting 0 (measure twice in time before trusting one reading). The report DECLARES the warm-up blind spot rather than hiding it. Also learned on the way: Ardour accumulates ~6 xruns/min even with nothing playing (67072 -> 67123 -> 67134 across captures minutes apart), which is exactly why only the session delta may ever be quoted. FIXED IN THE SHARED READER perf._read(None) raised TypeError instead of returning the default, so on any machine without a coretemp/dell_smm hwmon the thermal read CRASHED rather than reading "unknown" — the Bridge shares this code path. Guarded. Refactored _proc_cpu_rss into a pure parse_proc_stat() to make it testable, and it needed to be: pulsar's renderer comms look like `(pulsar) --type=renderer`, so splitting /proc/pid/stat on whitespace from the left shifts every field and silently reports some other column as CPU. TESTS: 71 new (tools/tests/test_gig_log.py), suite 211 -> 282, all green. Covers real pw-top/aseqdump lines, every baseline regression above, coalescing invariants, notes-survive-a-CC-flood, a torn final line costing one sample not the log, absent gear degrading to partial data instead of a crash, and sparklines bucketing by MAX so a one-second burst inside a 40-minute set cannot be averaged away.PLN (Algolia) authored -
docs(tasks): archive #71/#72/#53 — the LED lag was a representation bug, and it had a twin in the Bridge
PLN (Algolia) authored -
PLN, 2026-07-29: "midiGlobal can be killed imo. so yea please do the refactor of boottidal so we can free gain track1". `midiGGlobal = orDef 0.769 "^77" * 1.3` read the LIVE fader, and that was wrong twice over. First as SAFETY: it put a global gain on ONE physical fader, so brushing past fader 1 in the dark attenuated every midiG-using stream at once — the whole set, quietly, with nothing on screen to explain it. A control that can silence everything should not be reachable by accident. Second as ERGONOMICS: CC77 being read by Tidal is what kept the surface one lane off. Ardour has learned CC 78-84, CC77 is free, and with Tidal no longer reading it the eight faders can finally line up fader N -> dN instead of fader N -> d(N-1) (#46). Any track that reached for a fader was reaching one to the right of the orbit it was thinking about. Now a fixed pre-set: `midiGGlobal = 1.0`. Deliberately inaudible — the old untouched default evaluated to 0.769 * 1.3 = 0.9997, so every existing track moves by 0.003 dB. Global headroom is still adjustable, but it is one number in one place rather than something a hip can knock. NOT retired: the midiG family itself. PLN thought he no longer used it ("i dont use midiGs anymore iirc since ardour faders"), and behaviourally he is right -- with the global term gone, `midiG' ch l h` reduces to plain `gain (range l h ch)`. But the NAME is called in 167 files (`midiG'`) plus 12 (`midiG`), and deleting a definition the corpus references is a Haskell compile error, which takes the whole `let` block down and silences the entire track. That is the exact failure mode of the last two debugging evenings, and five days from OPAL is not when to re-open it. Retiring the usage is a post-gig corpus migration, sibling to #64. Verified with tools/check-boot.sh: the helper block typechecks against tidal-1.9.5 under `ghc -fno-code`, the #55 seed block typechecks, and all 13 g* helpers (including midiGdef, which is the one this edit could plausibly have broken) still yield events with an UNTOUCHED controller. Not "no error appeared" -- the helpers were run against an empty control map and their event counts checked. Also lands tools/at/lens.py: infers WHICH MEASUREMENT can see a given control from the track's own source, so a per-track acceptance test can assert "this knob measurably moves the orbit it is wired to" without using a lens that is blind to the effect. rms cannot see a filter or a bitcrusher -- they rearrange the spectrum and leave the level alone -- so an rms-based "did anything change?" reports a confident NO on an effect that works perfectly. Classifies 96.7% of the corpus's 5046 control bindings (33% unknown -> 3.3% once the scanner looked at a 3-line window instead of one line, because Tidal expressions wrap and an unclassified control is one the suite silently SKIPS -- precisely the controls most likely to be broken). 41 unit tests, every positive case a real corpus line, with a coverage guard so a future edit cannot quietly regress the scan.PLN (Algolia) authored -
Found by sweeping the rest of the gear for #71's bug class, which turned out to have a sibling living in the Bridge. `MidiStream._reader` did `q.put_nowait(ev)` and on `queue.Full` silently `pass`ed. That keeps 512 stale events and throws away the one that just happened -- exactly backwards for MIDI state, where the newest value IS the truth. A dashboard tab that stalled for ~1.3 s at 400 CC/s would fill its queue and then display frozen values for the rest of the set, with no error logged anywhere. Same failure shape as the LEDs: nothing breaks, it just quietly stops telling you the truth. Downstream, `_sse_midi` wrote and flushed once per event, so one fader sweep cost ~400 HTTP flushes a second per open tab, each with its own json.dumps. Fixed the way the LCXL painter was: drop-OLDEST on a full queue, and batch a frame's worth of events into a single write. New `coalesce()` folds the batch on one rule -- CONTINUOUS controls (CC, pitchbend, aftertouch) are STATE and may be superseded by a newer value; NOTES are EVENTS and may never be dropped. That distinction is the whole point: a fast monitor that loses a note is strictly worse than a slow one, so the tests assert every note survives a flood of 100 controller messages. Order is preserved by overwriting a superseded value where it stood rather than moving it to the end, so the monitor still reads as a timeline. The wire format is unchanged (one `data:` line per event), so ui/index.html's onmessage/JSON.parse is untouched. Also extracted `_fanout` so the drop policy is testable without ALSA. A green test on `parse_line` proved nothing about the queue behaviour behind it -- verify the seam. 8 new tests, 32 green in tools/bridge.
PLN (Algolia) authored -
PLN, watching the board for the first time on real hardware: "its slow to track if i move all fders quickly i see the animations lagging a sec or two behind". Not a slow device. Three compounding faults, each of which alone would have been survivable: 1. THE BUG. The read loop repainted whenever a CC *value* changed. But colour is a STEP function of value -- six steps in value_ramp, seven in filter_colour. Sweeping one knob 0 -> 127 emits ~128 events and can change the board at most 6 times. We were asking the wire for ~20x the work that could possibly be visible. 2. Every send re-resolved the port from scratch: `aconnect -o` AND `amidi -l` AND then `aseqsend`. Three forks per LED, ~1.7 ms each, measured. 3. All of it ran INSIDE `for line in proc.stdout`, so a write in flight stopped us reading the next MIDI event. aseqdump's pipe QUEUES rather than drops, so nothing was lost -- everything just arrived later, and later, without bound. That unboundedness is why it read as "a second or two" rather than a constant delay. The fix, in payoff order. A new `Painter` thread owns the wire; the reader only touches the model and hands it colours, never blocking. The painter dedupes on the COLOUR the board will show, coalesces so only the last colour per index within a frame reaches the wire, and batches every dirty index into ONE SysEx (the Launchpad dialect takes (index, colour) pairs, so a whole-surface repaint is a single write). Port resolution is cached for 2 s and thrown away the instant a write fails -- evidence, not a timer -- which keeps the replug-recovery property that made it uncached in the first place. Rate limiting is a floor on the GAP between writes, not a fixed tick, so an isolated button press still goes out immediately. Measured, not claimed. `--bench` replays PLN's own complaint (8 knobs + 8 faders swept together, 500 events/s) against a transport modelled at its real cost, and runs the legacy path beside the new one: legacy 744 wire msgs wall 11.23s overrun +8.23s coalesced 48 wire msgs wall 3.00s overrun +0.00s 15.5x fewer messages; latency p50 7.9 ms, p99 20.2 ms Overrun IS the visible lag -- it is how far behind his hands the board finishes. On the real device the per-write cost also fell 7.70 ms -> 2.72 ms with ports cached, so the total wire work is down roughly 44x. Twelve regression tests, no hardware needed. Speed regresses silently -- nothing goes red, it just gets slow again -- so the assertions are numeric: wire rate bounded by the frame rate and not the input rate, reader never falls behind, p99 under 50 ms, a lone press not delayed by the frame boundary. The last one is the one that matters: fast and wrong beats nothing, so we decode every SysEx the coalescer emitted through the mock surface and assert the board that LANDS is exactly what a full `build_frame` recomputation would have produced. Also: verbose logging now fires only when the board actually changes. Printing 400 lines a second of "CC77 = 63" was itself I/O in the hot loop, and told nobody anything.PLN (Algolia) authored -
Three days stuck on LED feedback that had never once been observed lighting a physical LED: every validation ran against a fake aseqdump stream. PLN confirmed the hardware works on 2026-07-28. Building the mock surface took under an hour and immediately explained the complaint that had survived two rounds of fixes — the DJ filters rest at BRIGHT AMBER, which is filter_colour(64) behaving exactly as designed and visually identical to the factory yellow he was trying to escape. Correct and looks-broken were the same picture.
PLN (Algolia) authored -
The LED work had a hole in the middle of it. `lcxl-leds.py --watch` was written, wired into the boot sequence and into gig-up.sh, and validated only against a FAKE aseqdump stream — nobody, human or machine, had ever seen it light a single LED. PLN kept reporting "still seeing static colors" and I had no way to check my own work, because I cannot look at the device. mock-lcxl decodes the same SysEx the real surface receives and renders it in the terminal, so the convention can be designed, reviewed and regression-tested without hardware. The only thing still needing PLN's eyes is whether the physical LEDs match the picture. It answered the "static yellow" complaint on its first run. Painting claude.tidal lights knobC 1-3 BRIGHT AMBER (63) — which is exactly filter_colour(64), the DJ filters resting at centre. Our code is working as designed; the design is the problem. Bright amber at rest is visually indistinguishable from the factory yellow PLN is trying to get away from, so "correct" and "looks broken" are the same picture. Second finding, same run: control_colour returns hue["full"] for every value from 8 to 119, so a touched knob is one flat bright colour across its whole travel. Touch changes it once and then it never moves again — which is precisely what he has been describing. So value_ramp() implements his spec verbatim — "from dim red nothing through bright red dim orange bright orange dim green bright green at max". That is six steps, and six is not a coincidence: the LCXL is bicolor (2 bits red x 2 bits green = 16 states) and only about six read reliably on a dim stage. The ramp uses the entire usable budget and spends nothing on shades nobody can tell apart. `mock-lcxl.py --palette` shows the whole budget; --ramp shows the convention resolving across 0..127. The DJ filters deliberately keep filter_colour and do NOT get the ramp: they are bipolar, centre 64 is bypass, and a monotonic red->green ramp would paint bypass as mid-orange and the two opposite musical extremes as the same colour. 8 regression tests covering the ramp order, monotonicity, "a mapped control at zero is dim, never off" (a control you cannot see is one you forget exists), "a filter is never dark", and a SysEx round-trip through the mock surface.
PLN (Algolia) authored -
Green unit tests on pure functions prove nothing about whether the rig makes sound. Every failure that has cost this project an evening lived in the SEAM: a helper that typechecks but silences an orbit, a knob that moves but changes nothing, an orbit still playing from the previous track. pytest cannot see any of it; a microphone can. So pv-at drives the REAL rig — boots a track through tidal-remote, taps the real PipeWire graph through probe-chain's `Tap`, and asserts on measured audio. Reusable by construction: a case is a named assertion, not a one-shot script, so tonight's investigation is tomorrow's regression suite. The fixture is a track, not a test tone: claude.tidal, a slow D-minor roller at 124. Test tones prove the signal path works and nothing about whether MUSIC survives it, and they are miserable to listen to while debugging. Each of its eight orbits is simultaneously a part of the arrangement and an assertion — kick/presence, hats/density, sub/register, stab/brightness, pad/sustain, arp/mask, riser/time-axis, break/mute. 8/8 passing against the live rig. Three of the assertions had to be rewritten first, and each rewrite is the same lesson in a new costume — the arithmetic is never the bug, the QUESTION is: * sub_is_low asserted on spectral centroid and failed d3 at 837 Hz — an orbit carrying `# lpf 220`. A magnitude-weighted centroid integrates the whole spectrum, so a low-level broadband floor spread over 20 kHz drags the mean far above where the energy actually is (d5: lpf 1400, centroid 6344 Hz). Replaced with band-energy ratio: d3 now reads 95% of its energy below 300 Hz. The centroid stays for RELATIVE tests, where the floor is common to both readings and cancels — filter_bites moves 2749 -> 2152 Hz cleanly. * riser_rises compared the first half of the window to the second and was a COIN FLIP: d7 is driven by a saw whose period is comparable to the capture window, so a fixed window lands on a random phase. Consecutive runs of the identical pattern scored +7.1 dB and -20.9 dB. Replaced with the maximum draw-up (largest rise from any bin to any later bin), which is true at every phase: 32.4 dB over a 34.1 dB span. * stab_is_bright demanded ">35% of energy above 1 kHz" and failed d4 at 32.6%. That threshold was invented, and the honest response to a number that close is to fix the question rather than nudge the threshold until it passes. Now comparative — the stab must sit well above the sub (32.6% vs 0.4%) — which is a musical invariant instead of a guess. Safety is structural, not procedural: SAFE_CC is an allowlist, CC 77-84 are excluded by construction (Ardour owns those faders; CC77 down is total silence), the LCXL port is re-resolved BY NAME on every send, and the suite hushes when it finishes. Exit code 2 means "rig not running", kept distinct from 1 "a case failed" — conflating an unmet precondition with a pass is how a green suite comes to mean nothing.PLN (Algolia) authored -
Three evenings this week went to bugs that were VISIBLE IN THE TEXT and that no eye caught mid-set, because none of them produce an error message. They produce silence, on stage: * desire.tidal — `off 0.125 (|+ note 12 . (|* gain 0.6))`. `.` binds looser than application, so this composes a FUNCTION into a ValueMap. GHC refuses the whole block, and since a blank line is Tidal's block separator, ONE character silenced nine orbits while the file looked perfect on screen. * you_my_sunshine.tidal — a local `let gF3 = (# djfbus 3 (range 0.05 0.95 "^51"))` shadowing the global helper we had already fixed. SuperDirt maps djf through linexp(0, 0.5, 20, 10000): bypass is 0.5, and 0.05 is a ~26 Hz low-pass. Four orbits (d5/d7/d9/d11) inaudible until a knob crossed centre. * the #55 mute-bomb — an untouched `^NN` yields NO events, so `range a b "^NN"` is `silence`, not a number. All three are static properties of the file, so they belong in a linter. Seven rules, each earned by a real failure: PV001 local djfbus shadow (with an autofix that deletes the line so the track inherits the corrected global), PV002 operator-section composition (autofix adds the missing parens), PV003 double-applied global gain, PV004 shared cut group, PV005 duplicate orbit in one block, PV006 sample index past the end of its folder, PV007 orbit inventory (the input to the orphan-orbit transition check, `--setlist`). Three of my own rules were wrong on first contact with the corpus, and the corpus caught all three — which is the argument for running a linter against 694 real files before trusting it: * PV002 was string-blind and called four mini-notation `.` separators compile-killers (`"k ~ ~ k . ~"` is grouping, not composition). 13 findings -> 0, all of them false. * PV003 claimed the whole 77-84 fader bank was "owned by Ardour" and raised 1109 errors. BootTidal itself reads `^77` (midiGGlobal); reading a fader CC is the documented convention. Narrowed to the actual hazard — ^77 applied twice — and demoted to a warning. * PV005 flagged every re-declaration of an orbit, but keeping several evaluable versions per file is normal live practice. Scoped to one block: 576 findings -> 32. * PV006 pooled all indices in an orbit against all folders and invented a finding on d8 (n=24 is org_jungle_breaks', not breaks165's). Also fixed the parser: ORBIT_RE demanded a same-line `$` and so dropped d5 from you_my_sunshine's inventory WHILE THE TRACK WAS PLAYING IT — PLN writes `d5 -- comment` with the `$` on the continuation line, which Haskell's layout rule allows. A linter that under-reports is worse than none. Corpus after the corrections: 694 tracks, 316 errors (all PV001 shadows, in archive material), 281 warnings. First real run found a live one — d5 and d11 of you_my_sunshine share cut group 5, so the voice and the chop truncate each other. 33 unit tests, every negative case a real corpus line that an earlier rule version wrongly flagged.PLN (Algolia) authored -
The full #66 entry for the documentary trail. The headline is not "all 10 tracks play" (they do, 91 orbits, zero silent) but that the instruments were wrong more often than the rig — six times, and the arithmetic was never the bug in any of them. Three answered a question about SHAPE with summary statistics, one divided by zero and called it an effect, one compared two points on the same hill, and one measured a system that included the measurement. The last is the best story: an xrun counter climbing +816 in 25 seconds, days before a gig, matching PLN's own report of crackles — and caused entirely by probe-chain's own PipeWire capture nodes. Remove them, repeat the identical musical load, get zero. Both REAL faults that day were compile errors wearing an audio costume.
PLN (Algolia) authored -
#12 (order the set into a BPM arc) has been blocked on three tempo questions, and they were framed as things PLN had to decide. Two of them are measurable. FIRST, ONE DISSOLVED ON INSPECTION. you_my_sunshine was recorded as "144 measured vs 166 written", a 22 BPM gap big enough to INVERT the planned rising finish Mafia(160)->Sunshine(166). The file has two setcps lines and the 144 one is COMMENTED OUT. The active tempo is 166, the backlog was right all along, and the "mismatch" was a grep reading a comment. That is the parsers-over-copy lesson again: a parser miss must never masquerade as a data conflict. (My own first re-extraction then mangled every number by using `tr -d '/60'`, which deletes the digits 6 and 0 — 160 became 1. Two parsing bugs in one sitting, on four-line shell one-liners.) SECOND, THE REAL QUESTION NEEDS AUDIO. gimme_acid declares 80 while the backlog calls it 160, and `setcps` cannot settle it: setcps(80/60/4) declares four beats per cycle at 80, but whether that is HEARD as 80 or 160 depends on what the patterns put inside the cycle. Half-time notation is standard in dnb. This decides whether the track sits beside the 160 BPM peak or is the second-slowest thing in the set. So: spectral-flux novelty curve -> autocorrelation -> the fastest pulse that explains the signal, reported together with its half/double family, because tempo from audio is only ever determined up to a factor of two and pretending otherwise is the error. BUILT THE KATANA FIRST, and it needed four passes — every one caught on synthetic click tracks of KNOWN tempo before the tool informed any decision: 1. Inter-onset-interval MODE. Failed on the first real track: gimme_acid's d1 is a continuous 303 line whose flux peaks every ~80 ms of internal movement, so the modal gap gave "750 events/min". Counting gaps between events cannot find a beat when the events are not beats. -> autocorrelation, which asks about PERIODICITY and is unbothered by extra onsets inside each period. 2. Octave errors: a clean 124 click read 61.9, a clean 160 read 80.0 — exactly half, correlation 0.85+, confidently wrong. Cause: at 100 Hz frames the true lag 48.39 must round to 48, which misaligns every later beat, while exactly 2x landed on a whole frame. -> 200 Hz frames + 15 ms Gaussian smoothing, so a fractional period still matches itself. 3. A uniform +1.8% bias (~2 BPM at techno tempo — enough to swap a 124 and a 127 in a set order). Suspected the unnormalised overlap in np.correlate, fixed that too, and the bias did not budge. The actual cause: the shortest-lag-within-12% rule, written to choose between octaves, was also sliding down the LEFT FLANK of the correct peak — comparing points on one hill as if they were different hills. -> candidates restricted to local maxima, plus parabolic interpolation. Result: 13/13 synthetic cases within 0.03%, including jittered, noisy, offbeat-8ths, and the half-time case (80 BPM with 16th kicks correctly reads 160 felt). A tempo number that is 2% wrong looks perfectly reasonable, which is exactly why this had to be calibrated against known truth instead of eyeballed against real audio.PLN (Algolia) authored -
The end-to-end gate result, plus the two findings that arrived after the log was first written: the mixer is clean LIVE (zero "Ardour eats it" across all 10 tracks, after PLN raised the physical faders mid-session), and the rig does not xrun under musical load — zero across 60s at maximum event density, scsynth ERR = 1 all session. Also records the fourth instrument error, which is the best story of the day: the xrun counter that climbed +816 in 25 seconds and looked exactly like "heavy patterns cause dropouts on gig week" was probe-chain's own capture streams. Remove the probes, repeat the identical musical load, get zero. Every probe run creates and destroys ~18 PipeWire nodes and each renegotiation costs Ardour an xrun. The pattern across all four: the arithmetic was never wrong. Three of them answered a question about SHAPE with a pair of summary statistics; the fourth measured a system that included the measurement. What makes them dangerous is that a perturbing instrument produces PLAUSIBLE numbers pointing at the wrong subsystem, and confirmation bias supplies the rest — I was looking for a crackle cause and was handed one.
PLN (Algolia) authored -
Third and last calibration of shape_of() against real audio. perfect.tidal's d4 measured |▇▇▆▆▇▇▆▅▆▆▇▆▃| — visibly steady for twelve bins — and was reported "DECAYING, 13 dB down, never recovering, almost certainly an xfade tail" on the strength of the thirteenth. The capture window had simply closed inside a gap between events. The midpoint-crossing rule added in 708c2aef cannot catch this: a dip in the last bin crosses the midpoint exactly ONCE, which is the signature of a genuine fade. So ask a different question — does the verdict survive dropping the final bin? If the inner trend is flat, the final bin WAS the verdict, and the answer is STEADY with the edge dip named explicitly rather than hidden. A real decay is unaffected, including one that only begins in the last three bins (still DECAYING). Eight synthetic shapes now separate cleanly: steady-with-edge-dip, monotonic decay, late decay, gated-riser-dipping-at-the-end, spiky-sparse, died, steady, rising. Pattern across all three fixes to this function: every false positive came from answering a question about SHAPE with a pair of summary statistics. Each fix replaced a statistic with the structural question that actually distinguishes the cases — does it recur, does it survive without its last sample. The verdict text now says which, so the reading can be argued with instead of believed.
PLN (Algolia) authored -
The pre-set checklist carried a line that has been wrong since gDJF was rewritten: "Faders 49/50 down (= bypass)". `gDJF` is two composed sections whose lpf and hpf are BOTH wide open at ch=0.5 and close as you move off centre in either direction (BootTidal.hs ~358, corrected mid-rehearsal 2026-07-27 when PLN described exactly that: "middle had no filter, total left = almost only subbass, total right = almost only superhigh"). So a knob at 0 is a hard low-pass. The runbook was instructing him to start the set with the bass-only end of the filter engaged — the opposite of bypass — and muscle memory built on a checklist is worse than no checklist. Measured the same day for confirmation: CC49 at 0 puts 72% of d1's energy below 150 Hz; at 127, 75% sits above 2 kHz; centre 64 is flat. Also adds §8, "I evaluated it and heard nothing", because the most common cause of silence on this rig is not audio: three times in one week a dead rig was a compile error. The identifying rule is structural and instant — ALL declared orbits silent means it did not compile (one block, one error), SOME silent means a real audio problem. With the bisect-by-blank-line technique PLN invented, the editor-buffer caveat, and a pointer to the new EVAL ERROR notification that finally makes this class visible. And wires the new instruments into the pre-set flow: check-mix.py (save Ardour first — it reads the file, and file vs live desk have disagreed in both directions), check-tracks.sh (the empirical compile gate, ~45s/track so run it the day before), lcxl-leds.py --map. Status section updated honestly: everything here was run against the live rig that day EXCEPT the LED paint, which nobody has confirmed by eye.
PLN (Algolia) authored
-