- 01 Aug, 2026 10 commits
-
-
Three closed tasks from the 2026-08-01 session, written for a cold reader. The through-line worth mining later: the rig SENSES everything and ACTUATES nothing. scsynth died and sclang logged it, the Bridge warned 63 times, gig-up saw it and printed GO — and separately, PLN's ears found a preload bug that four green tools had missed. Plus the session's sharpest lesson: two of my own fixes (a watchdog, a chmod) were themselves broken in exactly the way they were meant to catch.
PLN (Algolia) authored -
PLN's own rehearsal edits, 2026-08-01, saved before a break. Committed as their own human commit so nothing is lost — IN PROGRESS, not a finished arrangement. * ouais_je_funk — the kick becomes gate-driven instead of static: "[jazz,kick:5]*4" -> midiOn "^41" / midiOff "^41" variants over a layered "[techno:0,808bd:2,909,kick:4]" with gain 1.5 # lpf 400. Old "kick:5" kept commented rather than deleted. Also: the lead drops an octave — note "<b b b g>@7 <cs6!3 fs6>" is now ("..." - 12); the superimpose gate moves ^18 -> ^20; crushbus 101 added on ^18; and both d11 arpeggio blocks (pinkyup/thumbupdown, diverge/disconverge) are commented out rather than removed. * desire — crushbus 41 (range 16 2.5 "^52"). Left as PLN saved it. He resumes rehearsing Saturday afternoon.PLN (Algolia) authored -
--converge is the CLI big red button: bring the rig to ready, then let the existing checks prove it. Scope is SERVICES, not apps — it starts systemd units it owns and REPORTS Pulsar/Ardour with the command instead of launching them, because a script that opens windows under someone is a script they stop trusting (and Ardour has a session + crash-recovery dialog, #20). Launching apps belongs to the Bridge (#116). Safe at any moment: every action is conditional on the thing being broken, so a healthy rig converges to a no-op — verified, scsynth PID unchanged. It handles the exact 2026-08-01 failure explicitly: unit ACTIVE but audio server DEAD needs a RESTART, since `start` is a no-op on an active unit. Distinguishing those two is the whole lesson. Preload is fixed BEFORE SuperDirt boots, or the warm would not happen until the next restart. And it waits for scsynth to appear rather than guessing — guessing is what made the first watchdog flap. === THE THIRD TIME, AND THIS TIME IT WAS US === gig-up's first run (1219c704) found check-boot.sh and check-tracks.sh at mode 644 — never runnable as `tools/check-tracks.sh`. That fix turned out to be FAKE: the chmod only ever touched the working tree. Git has recorded 100644 for both from that day to this, so every fresh clone still got a broken pre-gig gate. And gig-up.sh had the same bug, from birth. Committed 100644 by 1219c704 and again by 6ab09eaa. It ran only because the local working tree happened to carry the bit; a `git checkout master` materialised it at 644 and it died with "permission denied". That failure was nearly invisible, which is the real story: the caller filtered its output through sed, so a run that never executed LOOKED like a clean no-op, and I reported "no-op confirmed" about a script that had not run. A false green produced by the absence of a program. So the gate now checks its own hands — "tools executable", HARD, verifying BOTH the on-disk bit and what git records. It found four real problems on its first run: disk=x git=100644 check-boot.sh <- day-one "fix" never committed disk=x git=100644 check-tracks.sh <- same disk=- git=100644 sc-watchdog.sh <- not executable AT ALL disk=- git=100644 tests/test-sc-watchdog.sh The third is the one that matters: parvagues-sc-watchdog.service had been ENABLED AT BOOT with an ExecStart it could not execute. It was stuck `activating`, restart- looping silently. The supervisor I added to catch silent failure was itself failing silently, and nothing would have said so until an audio server died at a venue and did not come back. All six now 100755 on disk and in the index; watchdog restarted and confirmed logging. Same family as the day-one finding: a thing verified once, in a context that no longer holds. The new twist is that "chmod +x" is not a fix — recording it in git is. Also honest-ing the recovery toast (#122). PLN reported needing a ctrl+enter in Pulsar to resume after a recovery. Likely ICMP port-unreachable poisoning Tidal's connected UDP socket (ECONNREFUSED on next send, stream stops until re-evaluated). Until #122 tests it, the toast says "if the music does not resume on its own, ctrl+enter in Pulsar" rather than claiming a full recovery — a rig that is alive and silent is indistinguishable on stage from the original fault. Validation: 16/16 green on the live rig (9 cold + 7 live). Converge no-op verified on a healthy rig; converge ACTION verified by stopping the watchdog and watching it come back, with scsynth untouched throughout.
PLN (Algolia) authored -
gig-up proved the set COMPILES and said nothing about whether anything could make a sound. scsynth was read only to colour a banner (:106,:115) and to skip --audio (:202) — it never failed. This morning the audio server had been dead for two days and the gate would still have printed GO. --live is a MODE, not the default. Cold-by-default remains right: cold checks are safe mid-set and must work on a train. Severity is chosen per link, deliberately: HARD scsynth alive / sclang :57120 / SC -> Ardour (>=24 pw links) no audio server, no listener, or no path to the DAW = there is no gig. SOFT Tidal :6010, LCXL -> SC, SuperDirt warmed, perf mode all legitimately absent ten minutes before doors; a gate that reddens for them is a gate PLN stops reading. Every check reads the thing that MAKES SOUND — a process, a socket, a real pw-link — never `systemctl is-active`. That distinction is the whole reason this exists: the unit reported active/running for two days while the rig was mute, because MainPID is sclang and scsynth is its child. ALSO, and it came from PLN's ears: "preload covers set", a HARD cold check. He heard crackles moving do_it_right -> take_5_drops. Measured in that window: 0 SuperDirt lates, 0 pipewire xruns, 17 lazy soundfile reads — all take5:*. Disk I/O, not CPU. The preload MECHANISM was healthy and said so ("47/47 banks OK in 1.9 s"); it was warming the wrong list. preload.scd was dated Jul 28, header "10 track(s) scanned", while the set is 13 — because setlist_samples.py reads setlist_opal2026.txt (10 tracks) and gig-up/cheat-sheet trust set-coherence.setlist_tracks() (13). The missing three are do_it_right, take_5_drops, electric_hammer: exactly the pair he was transitioning between. #120 tracks which source becomes canonical — not guessed here. Two properties made it invisible, and they are the transferable lesson: preload.scd is GITIGNORED (correctly — generated), so no diff ever showed it drifting, and NOTHING COMPARED IT TO THE SET. A generated artifact with no freshness check is a stale artifact waiting for the worst possible moment; its natural debut was a cold boot at the venue, on the first play of every track added since Jul 28. It only surfaced at a desk because SuperDirt had been up since Jul 28 with those banks cached from a first play weeks earlier, and a restart cleared the cache. tools/check-preload.sh compares BANK SETS (not bytes — the generator stamps a header and orders banks, so a byte diff would cry wolf), borrows set-coherence's parser rather than adding a fourth definition of "the setlist", and has --fix. Validation, all of it adversarial: * check-preload mutation-tested — restore the Jul-28 plan and it names the 7 banks that would crackle: daft fbreak120 hammer popkick realclaps superfreak take5. hammer and superfreak are independently confirmed: they appear as lazy loads in this morning's journal, before any of this was written. * --live on the live rig: 7/7 green after fixing MY OWN check (below). * THE GATE BITES: run under a PATH shim whose pgrep reports scsynth absent -> NO-GO, exit 1, correct fix hint, real rig never touched. That is precisely the state that printed GO this morning. * cold run unchanged: still GO, live chain not executed. One check was wrong before it was right, in the session's recurring shape. "SuperDirt warmed" first warned about a rig that was perfectly warm, because `journalctl --user -u parvagues-sc` returns lines from a PREVIOUS instance on this box — it handed back Jul 30 output while the current instance had already logged "51/51 banks OK". Now filtered by SyslogIdentifier and keyed to the unit's own ActiveEnterTimestamp, so it asks "did THIS boot warm?" rather than "was there ever a PRELOAD line", which any previous boot would satisfy forever.PLN (Algolia) authored -
PLN (Algolia) authored
-
PLN's own live-session edits, made at the desk on 2026-08-01 while rehearsing the OPAL set. Committed separately from the session's tooling work so the musical changes stay legible on their own. * wap — uncomment octersubsubbus 42 / octerbus 41, both on range 0 1.4 "^53". The sub layer is live again. (This is the `octersub` want noted in the 2026-07-31 set-coherence report.) * bombe_dj — "[808bd:1,808bd:11]" -> "[808bd:11]". One kick sample instead of two stacked, on the 1/4/6/9/11 positions. * do_it_right — mark d3's gM2/gF1 line with a TODO about buttons and variations.
PLN (Algolia) authored -
PLN (Algolia) authored
-
Minutes after test-sc-watchdog exited, PLN got a STICKY desktop notification: "3 restarts in 10 min. Giving up — this needs your eyes. journalctl --user -u scwd-test-1269280.service -n 50" scwd-test-1269280.service was the test's own FAKE transient unit. By the time the notification was read, it had been torn down — so the suggested command returns nothing, about a failure that never happened, while the real rig was playing fine. Two defects in one message. The test could reach a real notification daemon at all; and the give-up path is deliberately urgency=critical/sticky, which is correct for a real outage and actively harmful for a fake one — it cannot be dismissed by waiting. Fix: SCWD_NO_NOTIFY=1, honoured by notify() and set by the harness. A test must never be able to page a human about something that is not real. Re-run green: 7 passed, 0 failed, and silent. The log still shows the rate limiter walking (0 prior) -> (1 prior) -> (2 prior) -> GIVING UP, so suppressing the toast did not suppress the evidence — the assertion reads the log, not the notification. Same family as the flap it was written to catch: a supervisory tool whose side effects escape the scope it was reasoning about.PLN (Algolia) authored -
The rig went silent overnight and NOTHING recovered it. Seven layers watched it happen. From the journal: 10:37:36 PM: suspend entry (s2idle) 10:48:38 systemd-coredump: scsynth terminated abnormally 10:48:38 sclang: "Server 'localhost' exited with exit code 0." 11:18:47 Bridge: "scsynth process not found" x63, to the journal only scsynth is a CHILD of sclang and systemd's MainPID is sclang, so the unit reported `active (running)` for two days while there was no audio server at all. A green unit is not sound. Two findings behind it: * `parvagues-sc.service` was DISABLED. Six user units start with the session — gig-log, bridge, perf-tray, tidal-ardour-autoroute, lcxl-leds-watch, midi-autoconnect — and the only one that produces audio was not among them. It had been alive since Jul 28 purely as a manual leftover, which is why its death was permanent. Now enabled, and symlinked to the repo like the others so it cannot drift from the tracked copy. * The d8346672 fix (QT_QPA_PLATFORM=offscreen) worked exactly as designed: sclang survived the suspend perfectly. Suspend killed scsynth instead. The mitigation had been applied one layer too shallow — and because sclang stayed up, the unit never failed, so no Restart= setting of any kind would have fired. So: Restart=on-failure (bounded by StartLimitBurst=3/5min, honouring the unit's own "audio gear should never flap" comment) for the sclang case, plus an external watchdog for the scsynth case. Deliberately outside the boot path — a bug in start_and_midi.scd is gig-fatal, a bug in a watchdog is merely annoying. Folding this into an sclang ServerQuit handler is the right long-term shape; not seven days before a gig. THE FIRST VERSION FLAPPED ON PLN'S DESK, and the bug is the interesting part. The unit had `PartOf=parvagues-sc.service`, so that stopping SuperDirt would stop the watchdog. But PartOf propagates RESTARTS. Three lines, 90ms apart: 11:35:46.265 sc-watchdog[1102323]: scsynth GONE — restarting 11:35:46.321 systemd: parvagues-sc.service: Consumed 1min CPU time 11:35:46.354 sc-watchdog[1123545]: watching scsynth <- NEW PID The watchdog killed itself issuing the restart, Restart=always revived it, and its in-memory rate-limit counter reset to zero. MAX_RESTARTS was unreachable BY CONSTRUCTION: the state enforcing the limit was destroyed by the action it was limiting. It would not have stopped at four notifications; it would have gone forever. A supervisor must outlive its supervisee. Fixed three ways, defence in depth: - no PartOf on the watchdog unit (the actual fix) - restart log persisted to $XDG_RUNTIME_DIR, so being killed cannot launder it - await the server's return (up to 100s > TimeoutStopSec=90s) instead of sleeping a guessed 25s and judging a boot that was still in flight Also: every notification was urgency=critical, which Plasma treats as never-expire, so recovery buried the screen in sticky popups. Stickiness is now the signal — normal+timeout for "handled itself", critical+sticky only for "this needs your hands". Validation. tools/tests/test-sc-watchdog.sh runs the real script against a FAKE unit holding a FAKE child server, reproducing the green-unit-no-sound topology exactly. 7/7 pass in ~90s and scsynth's PID is unchanged across the run — it touches no real audio. It pins the flap directly: "watchdog survived issuing the restart", and the log shows (0 prior) -> (1 prior) -> (2 prior) -> GIVING UP. Plus a 25s --dry-run against the live unit: zero false positives on a healthy rig. That testability is the real lesson. v1 could only be exercised by killing the actual audio server five days before a gig, so its flap was found by a human hearing four notifications instead of by a test. UNIT and PROC are now injectable. The watchdog is committed but NOT enabled: it needs one real-rig recovery test before it earns a place in the boot set.PLN (Algolia) authored -
Two entries. #44's lesson is the one worth mining later: wiring six working checks into one caller found two of them dead, because every check had been verified once, interactively, by a human who supplied the missing piece without noticing. A human types `zsh tools/foo.sh` out of habit and never learns the file is not executable. An orchestrator has no habits. #114 records that the overnight constraint list did real work rather than being ceremony: "never write to a setlist .tidal" is what kept three ranked-"needed" tasks off the queue, and drawing that boundary first is why the night was productive instead of risky.
PLN (Algolia) authored
-
- 31 Jul, 2026 9 commits
-
-
The sweep landed after the brief was written. Replaces the placeholder with the measured figure and, more importantly, with the right DENOMINATOR: study/ is documentation prose, so 79+30 of 799 overstates it the same way the harness bug did. Also surfaces the CRASH class to him directly, since those are the ones worth his time and the ones I deliberately did not touch.
PLN (Algolia) authored -
The clean re-run after the harness fixes. Three numbers have now been attached to this question and the first two were both about the tool: #112 said 14 (a baseline taken during the mute migration) pre-fix run 128 (the column-0 `$` artifact, ~49 of them fake) clean run 79 BROKEN + 30 CRASH, out of 799 files And 799 is the wrong denominator too. Split by scope: OPAL setlist 13 files 0 BROKEN 0 CRASH live/ 694 files 51 BROKEN 21 CRASH ~10% study/ 59 files 13 BROKEN 3 CRASH sandbox/blocks/test 21 files 5 BROKEN 3 CRASH `study/` is Tidal DOCUMENTATION with inline examples — prose that was never meant to compile as a track. Counting it inflates the figure the same way the harness bug did, just more quietly. The number worth quoting is 51+21 of 694. THE GIG IS UNAFFECTED. Zero setlist tracks in either category, and gig-up is GO. The CRASH class is new and it is the valuable one. These tracks COMPILE, then throw when queried — invalid mini-notation inside a string, which is exactly why the type checker waved them through: "[d d] <d d d*2 d*4>]" unbalanced bracket "~ ulgab?:1" `?` before `:` "[0 .. 7 0]" a range with a trailing element "<0.75 .. 0.8 0.8 .. 0.65>" a sequence on the left of `..` Every one would fail live, because the scheduler queries every cycle. Until this run they were all reported as "FAIL — 0 orbit(s) SILENT". Deliberately NOT fixed here. None are in the set, and each one needs a decision about what PLN meant — "[0 .. 7 0]" has at least two readings and only he knows which. Triage is the deliverable; the edits are his call. Also records a counting caveat rather than hiding it: silent-eval prints a track's STEM, not its path, so stems that exist in several directories are marked as ambiguous instead of guessed. The real fix is repo-relative paths in silent-eval's output — noted on #112, not silently papered over.PLN (Algolia) authored -
Leads with 'nothing needs your hands', because the previous two mornings led with a fader he had to raise and that framing sets the whole read. Then the two things that shipped, then the one finding that changes something he was already told: #112's '14 tracks do not compile' was a harness artifact, and the harness was calling his own dominant writing style a parse error. Deliberately does NOT quote a corpus-wide count. The clean re-run was still going; the number lands on the task instead. Quoting an unmeasured figure is exactly how #112 got a wrong one.
PLN (Algolia) authored -
The previous commit fixed three bugs in the tool whose entire job is catching failures that make no noise. That makes its own failure mode the worst one available: saying OK about a track that does not work, or BROKEN about a track that does. It was doing BOTH, corpus-wide — and it stayed hidden because the 13 setlist tracks, the only ones anyone ever pointed it at, happened to dodge all three. Seven tests, all structural — no ghc, no rig, 0.06s: 1. a column-0 `$` is a CONTINUATION (PLN's dominant corpus style), plus a guard that indenting does not FLATTEN relative structure below it, plus one for the ordinary inline style — which gets a test precisely because dodging the bug is what let the bug live. 2. a non-zero exit with no SILENT line is a CRASH, and its stderr survives into the detail, because stderr is the only thing that says why. 3. the exit code answers "is anything wrong", never "is anything silent". MUTATION-CHECKED, because green tests on their own prove nothing. Each fix was individually reverted and the suite re-run: remove the continuation indent -> test_dollar_at_column_zero_is_indented FAILS key the return to silence only -> test_exit_code_is_not_keyed_to_silence_alone FAILS both restored -> 7 passed Test 3 is the one that matters. When crashes were split out of the silence count, the return value was briefly left keyed to `silent_only`, so a corpus where every single track failed to compile would have printed "OK — every declared orbit emits events" and exited 0. That is a false green from the gate, and a false green here is indistinguishable from a working set right up until the downbeat. Full suite: 229 passed.PLN (Algolia) authored -
#16 was ranked "bonus, can be 80/20ed". The cheat-sheet IS the 80/20: at OPAL he is alone in a field with a laptop and a 48-cell control surface. The FOH chain and the go-bag he can hold in his head. Twelve columns of CC numbers he cannot. GENERATED, NOT AUTHORED. A hand-written card is a copy of tools/lcxl_grid.py, and a copy drifts silently the moment the grid changes — which it did twice this fortnight. This reads the grid and the setlist directly, so a card that disagrees with the rig is impossible by construction. `--md` rewrites docs/GIG-CHEATSHEET.md; regenerate rather than edit. THE EMERGENCY BOX GOES FIRST The moment you reach for this page is the moment something is wrong. A card that opens with a reference table makes you read past your own panic to get to the answer. So the top of the page is eight failure modes and their remedy, each one a symptom he has actually hit: LEDs dark but knobs alive (USB OUT endpoint stall — replug), a track making no sound (it did not compile; one block, one error), an orbit that will not die (it is the previous track's pattern), mixer silent while Tidal looks fine (a fader; the desk wins on touch). TWO LAYOUT BUGS THE FIRST RENDER CAUGHT * the title bar was hand-padded and off by one — now ljust'd like every other line, so it cannot drift again * gF3 and gM3 hold 7 and 8 orbits, which overran a side-by-side two-column family layout and silently misaligned the block. Stacked instead. AND ONE COLUMN DELETED FOR SAYING NOTHING The set table first carried a "roles" column. Every track in the set carries kick+perc+bass+lead, so it printed the identical string 13 times and pushed the page past 80 columns. Replaced with the orbits each track does NOT declare — which is the fact worth having on stage, because an undeclared orbit is not silent, it keeps playing whatever the previous track left there. That column is the transition ghost list (#77), per track, at a glance. It also shows `perfect` declaring all 12, i.e. the one track in the set that cannot inherit a ghost. Verified: 0 lines over 80 columns, renders from a clean checkout, and the family map printed matches lcxl_grid.filter_family/mute_family exactly.PLN (Algolia) authored -
#112 said "14 corpus tracks do not compile". Running the harness over the whole corpus said 128. Neither number was about the music. BUG 1 — column-0 `$` continuations reported as parse errors PLN writes a lot of the corpus like this: d1 $ whenmod 128 129 (…) $ s "<k k*2 <k*2 k> k>" Pulsar sends a block wrapped in GHCi's `:{ … :}`, which suspends the layout rule, so that `$` at column 0 is a continuation and the track plays fine. `orbit_bindings` rewrites `dN` into `name = idcp` and emits the block into a real MODULE, where a column-0 `$` opens a NEW top-level declaration — a hard parse error. The harness then printed, in these words:✖ THE TRACK DOES NOT COMPILE — it will fail live too. about code that has never once failed live. The intent to indent was already written in the comment above the line ("becomes `name = idcp` + ` $ x`"); the indent itself was never applied. Two spaces on lines[1:] restores it and preserves every relative indent below. Why it stayed hidden: ZERO of the 13 setlist tracks use that style, and the setlist is what anyone actually runs. The harness was green on everything it was ever pointed at. Same shape as the chmod bug in the previous commit — a thing verified once, in a context that no longer covers the corpus. BUG 2 — a crashed probe reported as "FAIL — 0 orbit(s) SILENT" The Haskell probe sets its failure flag ONLY when it prints a SILENT line, so a non-zero exit with no SILENT line never measured silence at all — it threw while QUERYING. `check_track` mapped any non-zero return to "SILENT" and discarded stderr, so the one string that said what went wrong was thrown away and replaced by a self-contradicting verdict. New CRASH verdict keeps stderr. It immediately paid for itself. `live/chill/dub.tidal` compiles, plays three orbits, then throws: Syntax error in sequence: "<0.75 .. 0.8 0.8 .. 0.65>" ^ A real mini-notation bug, precisely located — and one that WOULD fail live, since the scheduler queries every cycle. It had been sitting behind a message that said zero orbits were silent. BUG 3 (latent, found while fixing 2) — a false green in the exit code Splitting the summary so crashes stop being counted as silence revealed that the return was keyed to the silence count alone. A corpus where every single track failed to compile would have printed "OK — every declared orbit emits events" and exited 0. The exit code now answers "is anything wrong", which is the only question a gate may answer — this is the one tool whose entire job is catching the failure that makes no noise. Verified: setlist still 13/13 green and gig-up still GO (so no regression on the path that matters); dub now reports THROWS WHEN QUERIED and exits 1; previously-"broken" tracks compile. The corpus-wide count is deliberately NOT restated here — it needs a clean re-run, and quoting a number I have not re-measured is how #112 got a wrong one in the first place.PLN (Algolia) authored -
A fortnight of debugging produced a shelf of good checks: check-boot, check-mix, silent-eval, fix-mute-roles, pvlint, orphan-orbits. Every one of them worked. Every one of them was also a command PLN would have to REMEMBER — and on Sat 8 Aug at ~19:00 he is in a field, alone, with a soundcheck window and no assistant. A check you have to remember under pressure is a check you do not run. So gig-up.sh contains no new checking logic at all. It is a running order, an exit code, and a decision about which failures are GO-BLOCKING. Its whole value is that there is now one thing to type. WHAT IS HARD vs SOFT, and why the line is drawn there HARD boot helpers one parse error in BootTidal.hs silences EVERY track at once, invisibly, while a stale ghci holds the old definitions. First check for that reason. HARD ardour faders earned it three sessions running, a DIFFERENT set of tracks each time: 07-28 had 05/06/08/10 at -inf, 07-30 had 06/10/12, 07-31 had 10. Three recurrences is not bad luck, it is a property of MIDI-learned faders that drift from the saved session in both directions. HARD compiles / mute map / pvlint SOFT transition ghosts real (#77) and known-open. A gate that goes red for work you have consciously deferred is a gate that gets ignored. SOFT LCXL present legitimately unplugged at a kitchen table. At the venue, a warn here means the set has no hands. TWO BUGS FOUND BY THE FIRST RUN — which is the argument for building it 1. check-boot.sh and check-tracks.sh were mode 644. NOT EXECUTABLE. The pre-gig audio gate has never once been runnable as `tools/check-tracks.sh` since it was written; it only ever ran when someone typed `zsh tools/...`. Every green memory of it is a green memory of a different invocation. 2. pvlint's `--setlist` is a MODIFIER ("these paths are ordered"), not a mode — it still requires paths. `python3 -m pvlint --setlist` exits 2 on an argparse error, which from the outside looks exactly like a lint failure. gig-up now borrows set-coherence's setlist_tracks() rather than teaching a fourth file to parse the setlist. Both are the same shape and it is this rig's signature failure: a thing that was verified once, in a context that no longer holds. Assembling the checks into one caller is what re-executed them under fresh conditions. DESIGN NOTES * Cold by default. Makes no sound, sends no MIDI, writes no .tidal. Safe to run mid-set, at 3am, or backstage. The audio gate is --audio, and it skips itself if the cold gate is already red — booting 13 tracks to confirm what a typecheck just told you is 10 wasted minutes at a venue. * Never touches CC 77-84 (Ardour track gains; 77 down is total silence) and never sweeps 93 (gPanic). * Scope is the SETLIST, not the corpus. 14 corpus tracks do not compile (#112) and must never make the gig gate red. * Failures print FIRST and carry their fix command inline, because the read order in a field is "what is broken / what do I type". * GO states its own limit every time: check-mix reads the SAVED session, so green means "Ardour will BOOT with this fader up", never "the mixer IS correct". Saying it in those words is how the -inf bug stops hiding behind a clean report. Verified: NO-GO path observed for real (2 failures, both genuine, both fixed); GO path 7/7 green; --help, --quiet and unknown-flag paths exercised.PLN (Algolia) authored -
PLN (Algolia) authored
-
feat(tools): set-coherence — 108 controls sit idle under his hand, and three of them are systematic (#113) PLN, 2026-07-31, having walked the OPAL set by eye: "when i went across set i saw some unmapped effects/button, a 'set coherence mapping review' and quick wins suggestions (bass octersub, lead slice, etc etc) could be good ways for you to fill-in suggest" He was right, and the number is larger than "some": **108 idle slots across 117 declared orbits in the 13 setlist tracks.** ## Why this needed a tool rather than a read-through Absence has no line number. The surface is a fixed grid — each orbit owns a known set of cells (effect knob, maybe a second, gate button, maybe a second) — so a track that declares d5 and never references ^33 leaves d5's knob inert for that track's whole duration. You cannot grep for a control that isn't there; you can only compute it, by subtracting what the track USES from what the orbit OWNS. The tool reports both directions of the same coherence question, deliberately: IDLE orbit owns a slot the track never uses -> unused affordance (new) STARVED track drives more controls than it owns -> already covered by PV008 and the FIXME(#54)/(#94) markers Fixing one can create the other, which is why they belong in one report. ## Three systematic findings, not a flat list 1. **The drums' own effect knobs are the deadest controls on the board.** d2.fx (B2 ^30) idle in 12/13 tracks, d1.fx (B1 ^29) 9/13, d3.fx (B3 ^31) 8/13. d1-d3 own exactly ONE per-orbit knob each — their row-C cells are the family filters — so this is not a spare slot going unused, it is their only knob. The KICK has no per-orbit effect knob bound anywhere in the entire set. 2. **Row A (d9-d12 effects, A5-A8) is ~90% idle.** d11 6/6, d12 5/5, d10 6/7, d9 5/8. That is #98's proposal (pool A5-A8 in order of appearance) with its supporting numbers: four knobs almost never used, on exactly the orbits that overflow. 3. **d6 has gates but NO knobs, 5 of 5 tracks.** Fallout from the d7->d6 consolidation: ^58/^90 came along, ^34/^54 did not. And vague_de_crime's d6 references nothing at all — no knob, no gate. ## Suggestions are grounded, not invented Measured what PLN actually binds to a per-orbit effect knob across the set — perc legato(4); bass lpf(3), gain(1), bandf(1); lead width(2), modIndex, filterRange, cutoff, resonance; kick nothing — and the suggestion table echoes that idiom before extending it (memory feedback_self_reference_rhythm_study: extract the grammar from his own tracks rather than importing generic Tidal advice). Worth recording because it inverts the ask: his two named wants sit OUTSIDE his measured idiom. He binds lpf to bass knobs and never octersub; slice lives on his buttons, not his knobs. So "bass octersub, lead slice" are additions he is reaching for, not corrections of drift — and saying so is more useful than pretending the corpus already implied them. ## Deliberately not applied Every suggestion changes how a track sounds and he is rehearsing on these files this weekend. Output is docs/2026-07-31-set-coherence.md to skim and cherry-pick. The tool also never suggests binding CC 77-84 / 13-16 (Ardour track gains), 93 (gPanic), or the family cells 49/50/51 + 73/74/75. Also confirms the #94 remap held: "borrowed" CCs — ones an orbit drives without owning — are 0 or 1 across every track in the set.PLN (Algolia) authored
-
- 30 Jul, 2026 7 commits
-
-
PLN (Algolia) authored
-
`# width (range 0.2 0.8 "^33") -- /!\ FIXME EXPLOSIF /!\` in gimme_acid's acid synth. PLN has had a hazard marker on that line for a while. The cause is mechanical and checkable without any audio: ^33 is an A/B knob, so BootTidal's #55 seed gives it 0. The seed policy states its own convention — "range <neutral> <extreme>, so 0 == neutral for effect knobs" — but 0.2 is NOT neutral for `width`: the preset reference PLN pasted two lines above the call says nominal is width = 0.51. So the line boots the synth at an extreme and the knob sweeps it THROUGH nominal rather than away from it, which is a good candidate for whatever "explosif" means to his ears. This is the exact shape BootTidal.hs already documents as an authoring bug, using piment_bresilien's `legato (range 0.05 2 "^52")` as its worked example, and its stated remedy is to fix the range in the TRACK rather than special-case the seed. Deliberately NOT applied. The candidate is `range 0.51 0.8 "^33"` — boots at nominal, knob only adds — but it costs the 0.2-0.51 downward half of his sweep, and that is a decision about how the track sounds. The analysis goes in the file next to his marker so the next reader inherits the diagnosis instead of re-deriving it; the sound stays his. Did remove one unambiguous thing: `# filterRange (range 0 8 "^53")` appeared TWICE in consecutive lines, same value, so the second was a no-op paste. MORNING.md rewritten for today. Leads with the finding that actually matters at J-7: check-mix.py reports Tidal 06/10/12 at -inf dB in the SAVED Ardour session, and cross-referencing those against the orbits each setlist track declares shows **9 of 13 tracks would lose at least one orbit on a relaunch** (#62). The live faders are probably fine — PLN has been playing — which is precisely the bug: the fix is not persisted, so it dies with the process. Ctrl+S is the whole remedy, and it is his hands, not mine: the D row is MIDI-learned to CC 77-84 and writing from outside desyncs it from the physical desk. Also flagged in MORNING.md rather than guessed: yesterday's file said "OPAL Day-5" and today's framing was "J-7". There is no OPAL 2026 entry in Web/www/content/lives/2026/, which CLAUDE.md names as the canonical source for gig metadata, so the countdown is unresolvable from here and is asked rather than invented.
PLN (Algolia) authored -
refactor(corpus): 917 lines move the kick onto its own mute button — every track, not just the set (#105, #70) PLN, mid-migration, when I had scoped this to the 13 setlist tracks: "good, and not only in the set am i right? did we not do the whole refact on the other .tidal files on my repo?" He is right, and the reason is muscle memory: his fingers do not know which folder a track lives in. A half-migrated corpus is worse than either extreme, because it makes the surface unpredictable exactly when he reaches for an old track mid-set. ## What moved 913 lines across 213 files, plus 7 orbits given a mute they never had: 505+4 gM1 -> gM2 percs vacating the kick's button (the BULK of the work — not the kick itself, which is only 133 lines) 127+1 gM2 -> gM1 the kick, arriving 66+4 gMute2 -> gM1 same, in the legacy long spelling 64 gM1 -> gM3 58 gMute3 -> gM3 spelling normalisation only 41+2 gM2 -> gM3 12+2 gMute2 -> gM2 / gMute1 -> gM2 / gMute1 -> gM1 / gM3 -> gM2 the tail 7 FILL orbits with NO mute at all, in the SET only Only the setlist was filled. PLN: "ensure we have that consistently in all set, and flag in other non-set tracks for me to see." Outside the set a missing gate may be a composition choice, so docs/2026-07-30-orbits-without-a-mute.md REPORTS the rest and writes nothing — split into the cut that makes 2397 findings actionable: A · partial 110 files 321 blocks uses family mutes but missed some <- the oversight he meant B · never 354 files 2076 blocks no orbit has one; predates the convention <- a style era, not a mistake ## Also, in this diff: #70 `live/midi/nova/dnb/liquid/you_my_sunshine.tidal` had d5 (the voice) and d11 (`no_sunshine:4/4` chopped) both on `# cut 5`. A cut group is monophonic, so every d11 event killed the vocal mid-word. That is ALSO the `-- FIXME VOICE OFF` PLN had left on d5 line 59: one root cause wearing two labels, found by pvlint PV004 on 2026-07-28 and confirmed by reading the file today. d11 moves to `# cut 11`, matching the group-N-equals-orbit-N convention d3/d6/d8/d9 already follow. The comment says how to revert in one line if the vocal was meant to be monophonic across both layers. ## How this was verified, given it touches 213 files PLN has to play in 7 days 1. **Convergence** — `--apply` twice; second pass proposes 0 rewrites and 0 fills. 2. **Structural proof, not spot-checks** — diffed every changed line against HEAD and asserted the two sides are identical once mute tokens and whitespace are stripped. 908 of 917 changed lines are provably mute-plumbing-only; the other 9 are listed and each accounted for (4 fills onto head lines with no `$`, 3 trailing-whitespace, 1 FIXME annotation, 1 the cut 5->11 fix). Nothing else in the corpus moved. 3. **Compile-invariance argument** — gM1/gM2/gM3 are the same type and arity in BootTidal, so substituting one for another cannot change whether a file compiles. Only the 7 fills alter expression structure, and all 7 are in the set. 4. **silent-eval --seeded on the set: 13/13 ok**, before and after. 5. Suite: 222 passed. THE HONEST LIMIT, stated because a green check here is misleading: every button CC seeds to 0 at boot and gMute<N> is `midiOn "^7N" (mask "f*16")`, so all three mutes are equally inert until pressed. silent-eval therefore proves nothing got silenced at boot and NOTHING about behaviour under a press — which is the only thing this change alters. The real gate is PLN's ear, or pv-at (#74). NOTE FOR PLN: 213 files changed on disk. Anything open in Pulsar now has a stale buffer, and Pulsar saves the BUFFER — reload before evaluating or your next Ctrl-S silently reverts the migration in that file.PLN (Algolia) authored -
PLN settled the gM question on 2026-07-30, and the answer was not the one this task had assumed for a day: "I want the kick, now on d1/fader1, to have its mute on F1 botrow button. I want the C1 knob to djf all percs, C2 only bass djf, C3 melodies DJF. however i want the mutes consistent across all all tracks: m1 d1 / m2 other percs / m3 all bass+melodics" ## The thing I had wrong #105 was framed as "align gM<N> to gF<N>" — make the mute index mirror the filter index so an orbit's knob and button share a column. I measured 35 mismatches in the set under that invariant and was ready to fix them. That invariant is WRONG, and the measurement was answering the wrong question. The filters and the mutes are two different gestures: FILTERS want the rhythm section as ONE BLOC — you sweep the drums together MUTES want the kick SPLIT OUT on its own button — you drop the kick alone So gF groups {d1,d2,d3,d8} and gM splits d1 from {d2,d3,d8}. Under the real map the edit count is 55, not 35 — and `d4: gF2+gM3`, which the mirror-invariant called a mismatch in 11 tracks, is CORRECT. Had I shipped the "alignment" I would have moved 11 correct lines and left the kick sharing a button with the hats. Recorded as tools/lcxl_grid.py `_FILTER_FAMILY` / `_MUTE_FAMILY` — in the ONE authored grid (#97), with his words above it, so the asymmetry reads as deliberate rather than as drift. `as_dict()` now exports `orbit_family` so the HUD can say which knob AND which button own an orbit; `--generate` rewrote tools/lcxl_grid.json and the HUD package's own copy (outside this repo, in Tools/pulsar-parvagues-hud). ## tools/fix-mute-roles.py Convergent migrator: `--apply` rewrites, `--check` exits 1 on drift (a pre-gig gate), `--fill` gives a mute to a block that has none, `--report-missing` writes a markdown report instead. Reads the map from lcxl_grid rather than restating it. ## PV012 / PV013, so it cannot re-drift PV012 flags an orbit whose gM does not match its ROLE — deliberately NOT "gM index != gF index", and there is a regression test asserting `d2 $ gF1 $ gM2` is clean, because a rule written to the mirror invariant would flag that valid line. PV013 flags an orbit with no family mute at all: PLN's "no mute is oversight!" — nothing on the surface can drop such an orbit, so taking it out means editing live. ## PV013 immediately caught a bug in the migrator that wrote it Minutes after PV013 existed, it flagged `d5` in you_my_sunshine — an orbit the migrator had walked straight past. Cause: fix-mute-roles had its OWN orbit regex requiring `^dN $`, and that file's head line is `d5 -- The Voice of Love`, no `$`, with the gates on a commented continuation. pvlint's regex allows `dN` followed by `$`, `--`, or EOL. Two parsers disagreeing about what an orbit IS is the same class of bug as two copies of the grid, so the migrator now imports pvlint's parser and owns none of its own. Re-running found 14 more rewrites and 7 more missing mutes it had silently skipped — the miss was under-application, not corruption, but it would have left exactly the inconsistency this task exists to remove. Also fixes #109: the orphan-orbits hand-measurement for you_my_sunshine still expected d7, which PLN moved to d6 in ce887b78. Re-derived by reading the file's column-0 declarations, not by pasting the parser's output — a hand measurement that quotes the thing it checks is a tautology. Suite: 222 passed, 0 failed (8 new PV012/PV013 tests).PLN (Algolia) authored -
Twelve files PLN had been working live, committed as his own change before any tooling touches them. Not a refactor: this is the set moving under his hands. The through-line is the #94 remap paying off. The remap had COMMENTED OUT every control line that fired across columns — honest, but it left orbits playing with their hands tied. He is now restoring them onto the CCs the orbit actually owns: gimme_acid d1's kick variations were disabled because they rode ^42, which the remap gave to d2. Restored on ^41, d1's own gate — so the kick keeps its midiOn/midiOff variation without borrowing a neighbour's button. perfect d5's SUPERSTAR chain was 14 commented lines. Rebuilt live on ^53/^33 with a nested `midiOff "^89"` mask, so the slice logic is gated by a button instead of hardcoded. desire the compile blocker is gone: `# pan 0.42plz /se` (line 46) is fixed. That one typo, in a file with no blank lines — hence ONE do-block — was killing every orbit in the track. Found by silent-eval learning to blame the track instead of itself; `silent-eval --seeded` now reports 13/13 ok, the first time the whole OPAL setlist has built cold. Also: d7→d6 in you_my_sunshine (with ^91→^90, ^59→^58) — the same consolidation he made in do_it_right, freeing an orbit slot. This invalidates a hand-measured fixture in tools/tests/test_orphan_orbits.py; the parser is right, the fixture is stale. Orbit naming throughout (BASSE DU 3e cercle, Synth lapin speed, SuperStars, Breaks divins), gain tuning, and backlog notes. Structural changes stay in the next commit so this one reads as pure performance work.PLN (Algolia) authored -
PLN (Algolia) authored
-
Four tasks closed 2026-07-29 evening: the silent-eval harness fix (and the real bug it uncovered), the take-pack sidecar+archive tool, and the desire rhythm study. Written for a cold reader per the archive's own convention — commit hashes are in git log, this is the narrative and the numbers that don't show up in a diff.
PLN (Algolia) authored
-
- 29 Jul, 2026 14 commits
-
-
The existing MORNING.md led with a warning about ctrl+enter fading to silence over 4 bars (#79) — fixed and confirmed by ear hours ago ("no sample goes oblivion!!👏 "). Leading with a solved problem buries what actually needs review tonight. Replaced with five things worth his eyes/ears, ordered by urgency: the desire compile break (#108, a real gig blocker), the corrected take-94 mix (v2, the trim threshold fix), silent-eval now covering all 13 setlist tracks, the slop renderer's first real output (not yet good enough to send, flagged as such), and the four desire bass-rhythm alternatives to A/B. Everything else from the session is mechanical and needs no listening, so it's kept to one short list at the bottom rather than mixed in with what actually needs his judgment.PLN (Algolia) authored -
PLN: "i gotta solve desire bassline is not great rythmics coulld be cooler more nto more ete_a_mauerpark or insouciance or haunted_house i guess." Read the three references he named and extracted what they share. The diagnosis turns out to be mechanical rather than a matter of taste. desire's d4 is note (scale "aeolian" "0 . 0*<1 2> . <0 3> . 0*<1 1 4>" + 2 - "[24,36]") — four EQUAL groups, the same degree in nearly all of them, never a rest, never displaced. The only variable is density, applied to a repeated note. That is a metronome with a stutter, and no amount of filter movement makes it a riff. The six mechanisms both references use and desire has none of: 1. uneven durations via `@` (insouciance 5:1:1:1 of 8; mauerpark 1:1:14 of 16) — one long note holds the harmony, short notes carry the motion; 2. the pitch pattern IS the rhythm pattern, so a new note is a new onset, instead of a static degree with density stacked on top; 3. calage — `(0.125 <~)` displaces the whole line so it pushes against the kick rather than doubling it; 4. accent follows rhythm — `|* gain ("1@5 0.95 0.98 0.95")` carries the SAME weighting as the notes; 5. space — rests inside the phrase and whole bars masked out; 6. the phrase is longer than the bar (4-bar alternation, plus `slow 2`). Four proposals in study/desire_bass_rhythm.tidal. A, B and C each isolate ONE mechanism so what he hears is attributable; D combines all six. Pitch design is held constant everywhere — aeolian, +2 root, "[24,36]" double — because the key was measured at corr=0.903 and is not what he is unhappy with. All four verified through silent-eval --seeded: they compile and every one emits events cold. Measured onsets per bar: B 3.0, A 4.0, C 4.5, D 5.75, against ~5.5 for what desire has today. D is deliberately DENSER than the current line and still reads as a riff — which is the control that shows the problem was never "too many notes", it was where they fall and how long they last. desire.tidal is untouched: it is in his dirty working tree, and it also does not currently compile at all (#108).PLN (Algolia) authored -
PLN has said repeatedly he is bad at socials and hoped slop would carry that weight. The plan for this has been complete since the clip planner landed; the renderer had never been written, so nothing could actually be produced. It can now. `visuals/slop/render_slop_clip.mjs` takes either a planned cut (`--idea desire --cut 90`, from clip_ideas.json with its measured tilt/kick-density/novelty and playset choice) or an arbitrary window of any file (`--audio ... --start ... --dur ...`), and renders a vertical 1080x1920 reel with Slopmotion reacting to that audio. DESIGN NOTES WORTH KEEPING - It lives in THIS repo, not hexa. hexa is Kevin's project and its main is clean; the script borrows its node_modules through createRequire instead of being committed into someone else's tree. Its scratch files go in hexa/public/__slop and are removed in a finally block — nothing is left behind in his working copy. - Capture is REALTIME via CDP screencast, not per-frame screenshots. Hydra's animation clock is wall time, so frame-stepping plays motion back ~3x fast and drifts away from the audio it is supposed to be reacting to. Screencast frames arrive timestamped and become a vfr->cfr concat, so a dropped frame is a slightly longer one — invisible — rather than a wrong clock. - The bands are computed from the REAL audio through an AnalyserNode, not from the synthetic pump the FX-preview script uses. Kick is a fast rise in the sub band above its own slow running average, because an absolute threshold would need retuning per track while a relative one rides the mix. - Backdrop clip choice is a hash of the job name, not Math.random: two renders of the same cut must be comparable, which is the entire point of a review pipeline. - The viewport is 9:16 natively rather than a 16:9 scene centre-cropped afterwards, which would throw away most of the motion. TWO ENVIRONMENT PROBLEMS FOUND AND FIXED WITHOUT COLLATERAL - The hexa checkout could not boot at all: 9 declared dependencies were missing from node_modules, so vite 500'd on @supabase/ssr and @vercel/speed-insights. `npm ci` refused (lock out of sync with package.json), and plain `npm install` would have rewritten package-lock.json — a TRACKED file in Kevin's repo. Installed the 9 with --no-save --no-package-lock instead; his tracked files are byte-identical after. - Playwright 1.60 wants chromium build 1223 and the cache has 1217. Rather than pulling ~150 MB into PLN's shared browser cache unasked, the script finds the newest full chromium already present. Full chromium, not chrome-headless-shell, which ships without the GPU/ANGLE stack this needs. WHAT IS NOT DONE: frame rate. 7.0 fps at 1080x1920 under swiftshader — the page itself renders at 7 fps, so it is the GL backend and the machine, not the capture. The two follow-up measurements (hardware EGL, and half resolution) both came back at 1.1 fps, which is not a verdict on either: load average was 12.5 with Pulsar burning a core from the #7 marker leak. They measure the machine. Recorded in FEEDBACK.md with an explicit warning to re-measure on a quiet box before concluding anything — a false workaround adopted from a contended benchmark is very hard to remove later. Deliberately not pushed further tonight: a sustained software-GL render is a CPU load test, and those do not run at night here while the rig is up.
PLN (Algolia) authored -
Four of thirteen setlist tracks could not be built, so their orbits had never been verified cold. Two causes, both the same shape: THE HARNESS DIVERGED FROM THE BOOT ENVIRONMENT, which is what #93 predicted would keep producing new errors. 1. Ambiguous `cutoff`. BootTidal defines `let cutoff = pF "cutoff"`. In ghci a let binding SHADOWS an import, quietly and legally. The harness dedents that block to module top level, where there is no shadowing — GHC reports "Ambiguous occurrence" and the track becomes unverifiable for a difference that does not exist on the rig. Fixed by hiding every name the generated module defines from the Context import, with the hide-list DERIVED from the generated text rather than listed, so it cannot drift when BootTidal gains a helper. 235 names on the current boot. 2. Overlapping IsString instances on a chord literal ("<gb3'maj db3'maj bb2'min bb2'maj>" could be ParseBP's `Pattern a` or Simple's `ControlPattern`). ExtendedDefaultRules and NoMonomorphismRestriction are ON BY DEFAULT IN GHCI, which is where these patterns actually run. Turning them on matched the environment; annotating each literal would have treated the symptom one track at a time forever. === And then the third failure turned out not to be a harness limit at all `live/collab/raph/desire.tidal` line 46 reads `# pan 0.42plz /se`. It is committed, not a stray working-copy edit. `plz` is not a Tidal function and the file has NO blank lines, so it is one single do-block: that one typo means every orbit of desire is dead on ctrl+enter. It is on the OPAL setlist, six days out. The tool had been reporting it as "BUILD FAILED — harness limit, NOT a track verdict". That message was written to protect the music from the tool, and it was right twice and catastrophically wrong the third time: it told the reader, in bold, to ignore a broken setlist track. A parser miss must never masquerade as a data conflict, and the inverse is worse. So the verdict is now computed, not asserted. Errors whose line numbers fall inside the track's own generated lines get a distinct BROKEN verdict — "THE TRACK DOES NOT COMPILE — it will fail live too" — and count as a gig blocker. Errors in the boot helpers or the scaffolding stay a harness limit. Line numbers are also mapped back to the real .tidal by matching the offending line's TEXT, because a generated-module line number is useless to whoever has to fix the file and looks authoritative while being so; unmatched or ambiguous lines are left alone rather than guessed at. Result: 12/13 build and every declared orbit emits events cold. The 13th is a real bug in the music, reported as one, with a file:line you can jump to. NOT FIXED HERE deliberately: desire.tidal is in PLN's dirty working tree and Pulsar saves the BUFFER, so a disk edit made behind his back can be silently reverted by his next ctrl+S. It is item 1 of MORNING.md instead, with the one-line fix, to be applied in the editor where it will actually stick.PLN (Algolia) authored -
Take 94 came back with no track trail, and there was no way to tell from the logs whether the watcher had seen the track changes, failed to bind them, or simply never run. It had been running for 2h46m. It had emitted nothing but systemd's own start/stop lines the entire time. Two independent causes, and either alone is enough to blind the unit: 1. Python BLOCK-buffers stdout when it is a pipe, and under systemd it always is. Even the lines that were being printed would have sat unflushed in a 4 KB buffer. Fixed with Environment=PYTHONUNBUFFERED=1 in the unit. 2. `--watch -q` suppressed the track-change line itself, so there was nothing to flush. That is the wrong thing to make quiet. `quiet` should mean "no per-CC chatter" — thousands of lines an hour — not "never say what you are painting", which is a handful of lines an hour and the entire purpose of the daemon. The track-change log is now unconditional. Verified live rather than by reading: restarted the unit, wrote a different track to ~/.cache/parvagues/current-track, restored the original, and read the journal back. Both transitions appear with their binding counts (desire 28 controls, vague_de_crime 20), and a re-parse of the same file is labelled distinctly from a real track change — that distinction matters because an edit-and-save must keep the touched-state while a genuine track change must clear it. Same family as the stale-binding pattern this rig keeps producing: something resolved once, invalidated by an event, with no error anywhere. Here the error reporting itself was the thing that had been silently resolved to /dev/null.
PLN (Algolia) authored -
Two things in one file, both from the same evening's practice recording. === PART 1: `pack` — the performance must travel with the audio (#106, #107) "ensure recordings have the midi too, keep this from blowing up tracking either ardour rec or compressing to avoid storing hours of no-move?" Today the audio is permanent and the performance is not. Ardour's sources live forever under the session; the MIDI and the track timeline sit in a gig-log directory that prune() deletes on a timer. So the 29862 CC events that explain take 94 were on a countdown while the audio they explain was not. `pack` writes a sidecar next to the audio: meta.json (window, sr, subtype, per-orbit peak dBFS, silent orbits, xrun delta, gear + boot state), midi.jsonl (every cc/note/track/mark record in the window, copied verbatim out of the log so pruning cannot reach it), and edl.json. The sidecar states its own honest limit in a field: gig-log COALESCES CC per (port, channel, controller) per second, keeping count + first/last/min/max. That is a faithful summary of a knob sweep and is what makes the log cheap, but a .mid rendered from it would be a reconstruction, not a recording. The JSONL is the truth. `--compress` then wavpacks every source and verifies each one with `wvunpack -vm` before reporting a single byte saved — an archiver that reports a ratio without checking is reporting a hope. The .wav is NEVER deleted; removal stays a separate human step after the take has been heard. FLAC was rejected outright: it cannot store 32-bit float, and routing through s32 would hard-clip the 8 orbits sitting over 0 dBFS while looking like a win on the silent one. Two measurement bugs found by running it on real data rather than trusting it: - xrun is a RUNNING TOTAL in gig-log's `s` records, not a per-tick delta (its own CUMULATIVE tuple exists for exactly this mistake). Summing it reported 58,668,469 xruns for a 21-minute take. Rebased to last-minus-first: 230, a real number. - Records are not events. A `cc` record is a one-second bucket carrying `n`, so printing the record count understated the performance 25x (1167 vs 29862). === PART 2: the trim threshold was protecting every bad transition PLN heard the first machine-trimmed mix and found the hole: "silence around 18 is not normal its a bad transition ahah in a real mixing work wed cut it proper and fade or at least trim most". The gap he heard WAS detected and reported, and survived anyway. SILENT_RUN_S was 20 s; the three gaps that made it into the mix measured 17 s, 18 s and 11 s. Every one sat just under the bar. A threshold chosen so the trimmer could never eat a musical break had silently become a threshold that preserves every failed transition instead. Three changes, all of them his rule rather than a tuning: 1. CLAMP, don't binary-cut. The bar drops to 10 s, but a detected silence is no longer removed whole — the last 2 s before the music resumes is kept as a breath. That is "or at least trim most" implemented literally, and it makes the lower threshold safe: the worst case if the rule fires on something musical is now a shortened rest, not a missing one. classify() computes both the detected region and the removed region so the report and the mixer cannot disagree. 2. FADE every seam. Every cut this tool had ever made was a butt-join, which in a live room is a click. Each removal now fades out into the cut and back in out of it over 250 ms, applied sample-accurately — the old mixer decided keep/drop per one-second block by its midpoint, quantising every boundary to +-0.5 s. Verified on the re-rendered take: max sample-to-sample step at the five seams is 0.0001-0.0029 against a p99.99 of 0.0562 for the take as a whole, i.e. the joins are 20-100x below ordinary musical transients. 3. BRIDGE blips. A dead patch is rarely clean end to end, and one orbit's tail crossing -60 dBFS for a second splits it into pieces that each fall under the bar. Only single-orbit blips are bridged: a real hit lights several orbits, and that is what keeps this from swallowing a one-shot. Result on take 94: 143 s trimmed instead of 119 s, all five gaps closed including the one at +18.0 min he named, output 18.75 min, peak -3.36 dBFS. === `selftest` Added because two of these rules could not be validated on real material — the bridge fires zero times on take 94 and a correct fade is inaudible by construction. A rule you have never seen fire is a hope, not a rule. It found a bug on the first run, in the test's own arithmetic: asserting "no span survives a multi-orbit hit" was wrong because the left half is exactly SILENT_RUN_S long and qualifies by itself. The property that actually matters is that no trimmed span ever CONTAINS a moment when the music was playing, and that is what it now asserts. Correction to a verification, worth recording: the first check of the re-rendered mix reported an 11 s silence still present. It did not exist. I measured the output against -60 dBFS while the mix carries a uniform -16 dB, so I was testing at an effective -44 dBFS at source. At the matched threshold exactly one rest over 4 s remains, 9.25 s, correctly left alone as musical. The right lens per control, again.
PLN (Algolia) authored -
First listen to the auto-trimmed take-94 mix. The verdict on the sum itself is good ("great mix itself its clean") which clears the record-bus routing of suspicion: 12 orbits summed with a flat -16 dB keeps the balance he actually performed. The correction is the valuable half. He flagged the silence around 18 min as "not normal, a bad transition — in a real mixing work wed cut it proper and fade or at least trim most". That silence WAS detected and reported by take-lens, and survived anyway: SILENT_RUN_S was 20 s and all three surviving gaps measured 17 s, 18 s and 11 s. A threshold chosen so the trimmer could never eat a musical break had become a threshold that preserves every bad transition instead. Two rules extracted, both more general than this take: clamp long silences to a short breath rather than choosing between keeping all of it and removing all of it; and fade every cut seam, because every removal this tool has made so far was a butt-join. Also logged: the stretches he heard as "stuck in loops" are precisely the `loop` kind the classifier already flags and deliberately does not auto-cut.PLN (Algolia) authored -
Added `mix` (sum the orbits to one gain-corrected, trimmed stereo file), then found two real bugs in the trim rule by verifying my own render instead of trusting it. BUG 1 — the rule demanded idle hands AND silence audio_lens profile on the rendered mix reported rms -180 dBFS at 19.2 min: DIGITAL silence, sitting in the middle of a "trimmed" master. The first rule only cut where the hands were idle AND nothing sounded, and there PLN was busy on the controller with nothing playing. That third state — fiddling in silence — IS the practice/ plugging-in the trim exists to remove. Silence needs no corroboration from the hands, so it is now its own kind with its own (much shorter) 20s threshold: long enough to never eat a break or a drop's pre-silence (~4 bars, under 8s at these tempos), short enough to catch the real thing. BUG 2 — and the reason it was invisible: analysis ran at DISPLAY resolution A direct 1-second measurement of the same mix found 103 seconds of silence in five runs of 11-31s. The classifier, bucketing at 15s, found NONE of them: a 17-second silence straddling two 15-second buckets leaves no bucket fully silent, so every bucket's rms sat far above -60 dBFS. The threshold was fine; the grain was lying. Analysis now always runs at GRAIN_S = 1s and `--bucket` only sets the sparkline width. Detection resolution must never be a display preference — the coarse view is for eyes, the decision is for data. STRUCTURAL: one rule, one reader `classify()` is now the single source of the cut rule and `analyse()` the single audio pass, both shared by the report and the mixer. They were duplicated, which is the worst possible arrangement here: a mixer that cuts what the report called `keep` destroys material silently. Also dedup candidates by 80% OVERLAP rather than strict containment — the hands-idle run began 1s before the silence run on take 94, so containment reported the same stretch twice and the table contradicted itself. VALIDATED end to end on take 94 (21.14 min, 12 orbits) candidates 5, no contradictions: 3x silence (21s, 30s, 68s) TRIM, 2x playing (68s at 8.00 and 3.72 orbits avg) KEEP. Trimming only `silence`+`dead` -> 19.15 min out, peak -3.36 dBFS, rms -24.47. Re-measured the render: silence 103s -> 52s. The three survivors are 17s, 18s and 11s — all under the 20s threshold BY DESIGN, plausibly musical gaps that want an ear rather than a rule. Spectral balance broadband at 60s/300s/600s/900s via audio_lens profile (the opening 20s reads kick/sub-only because it genuinely is). `loop` is deliberately NOT cuttable: a droning pad under idle hands can be an intro as easily as a distraction. It proposes; the ear decides.
PLN (Algolia) authored -
PLN asked for "a master split per track loaded time, cleanup parts obviously 'practice or plugging in' that are scarce VS intro/outro (can we detect this? did we record play metadata as the ardour records?)". Three datasets existed and had never been joined: - Ardour names sources `Take<N>_Tidal <orbit>-<seq>%<L|R>.wav`, so the take number plus the frame count plus the mtime give an exact wall-clock window with no session-XML parsing at all. - gig-log has coalesced MIDI CC at 1 Hz: 29862 events over this take, on 46 distinct controllers. Hands-idle is the clearest signature of "plugging in". - per-orbit RMS/peak says what the SOUND did. The third one is not optional, and that is the design insight. Idle hands ALONE is ambiguous: you can play a part for three minutes without touching a knob. So a candidate is classified by what the audio was doing underneath it — `dead` (nothing above -60 dBFS: safe to cut), `loop` (a couple of orbits droning, hands off: probably plugging in), `playing` (most orbits going: a real part, KEEP). On take 94 that distinction earned its keep immediately: of three hands-idle runs, two were real parts at 8 and 4 orbits average, and only the 75s tail was actually dead. FINDINGS ON TAKE 94 (21.1 min, 20:08:33 -> 20:29:41, 12 orbits) - 8 of 12 orbits peak OVER 0 dBFS, worst d4 at +9.63 dB. The files are 32-bit FLOAT, so this is headroom and not damage — a gain cut recovers it exactly, and it only becomes real clipping on integer export or at the master bus. Labelling it "CLIPPING" would have been a false alarm sending him to re-record material that is completely intact, so the tool now reads the subtype and says which it is. A uniform -16 dB on the record bus lands the hottest orbit on the -6 dBFS source target. - d12 silent for the whole take (-78 dBFS). - no track timeline: nothing published the loaded track while it recorded. The tool says so plainly instead of guessing, because a confident wrong boundary is worse than none — the existing Bandcamp splits carry crossfade bleed for exactly that reason. Takes from d5baca32 onward split exactly. A MEASUREMENT TRAP, RECORDED IN THE CODE The first pass estimated duration as size/(sr*3), assuming 24-bit. These files are 32-bit float. That put take 94 at 28.2 min starting 20:01:30 instead of 21.1 min starting 20:08:33 — and manufactured a "6m15s dead intro" that never existed, purely from a 7-minute window shift. The fallback now assumes 4 bytes and, more importantly, exact lengths come from the frame count and estimated ones are labelled "do not cut from it". Also: logs_covering() checks EVERY log's [first,last] range rather than assuming the newest one holds the window, because a take can straddle a recorder restart — which is exactly what happened today. It proposes, it does not cut: output is a report plus an EDL JSON for armada/tide-table/edl_render.py. Trims are a taste call and the ear is the gate.PLN (Algolia) authored -
PLN recorded an afternoon of OPAL takes, then asked for "a master split per track loaded time". The answer should have been trivial. It was not: nothing in the rig recorded WHEN each track was loaded. The gig log had 78222 samples of thermals, xruns, gear and 7429 coalesced MIDI CC events for today alone — and no way to say what any of it was. A CC 41 is meaningless on its own, because the LCXL map is PER TRACK: on one track index 41 is a crush bus, on another it is a break gate. The performance data was all there and none of it was resolvable. Meanwhile the exact ground truth was sitting in a 33-byte file the whole time. Pulsar has published `~/.cache/parvagues/current-track` since #84 (2576e77) for the LED watcher to follow. Nobody was reading it into the log. WHY THIS MATTERS MORE THAN THE SPLIT IT WAS ASKED FOR Splitting a livecoded set by ear is genuinely hard, and we have the scars: orbit roles stay stable across tracks so level/presence detection fails, and every dN is a 4-cycle crossfade that bleeds the incoming track into the outgoing one's tail — the existing Bandcamp splits carry that bleed. A `k:"track"` line replaces all of that inference with a timestamp. Boundaries stop being clever and become read. And it retro-actively upgrades the CC stream from telemetry to an annotated performance score: track + CC + time resolves, through the per-track LCXL map we already generate, to "d4's DJ filter opened at 18:42:13". That is the input for telling practice apart from performance (hands-off vs hands-on density), for finding the moment worth clipping, and eventually for the emotion/timeline bundle. IMPLEMENTATION — deliberately the boring version - `_read_track()` never raises: absent file (publisher not up yet) or a short read both degrade to None. A missing track must degrade the log, never stop it. - Written ONLY on change, so a 3-hour session costs a handful of lines instead of 10800 repeats of one string. The header carries the initial value, so a session that never switches still has one authoritative statement of what was loaded. - Polled, not watched — for the same reason the LED watcher polls. One stat+read of 33 bytes per second is unmeasurable; an inotify watch adds a dependency and a whole class of "the watch silently died", which is the exact failure mode this data exists to prevent. - `load()` folds track records into `marks` so they render in the existing timeline for free, keeping `k:"track"` so a splitter can tell an automatic boundary from a human annotation. VALIDATED - `selftest` PASS; sampler + both readers still 0.66% of one core over 6s, well under the 3% observer budget (the observer must not perturb — we already learned that lesson when probe-chain's own capture streams caused the xruns). - END-TO-END, not just unit: ran the real recorder against a temp track file, moved it twice, and read the log back. Header carried the initial track; both changes appeared exactly once with their `from`, each within the 1s poll. Green tests on a pure function would not have proven the value reaches the file.
PLN (Algolia) authored -
#46 #90 #94 #97 #100 #84 #35 off the active board, each with the non-obvious part kept: the 32 role inversions phase 2 shipped and how they were repaired, why the FX bus rule is PV011 and not PV010, and the two independent highlight bugs (a class name that never matched, and an outline fix written in the one stylesheet copy that could not unset the other's border). Narrative in log 019.
PLN (Algolia) authored -
Captain's-log entry for the 2026-07-29 session, written while it is fresh (the armada/tasks discipline: these logs are the raw material for posts and video narration, not bureaucracy). Two findings, one class: a machine check stayed green while the instrument was broken. One was Tidal's (xfadeIn turns the outgoing pattern down but never off, and a gain-0 event still broadcasts its cut group). One was mine, shipped the previous evening (the button remap assigned cells in reading order, inverting 32 gate/gesture pairs that only PLN's ear caught). The shareable core, for later: a gain of zero is not the absence of an event. In a system where events carry side effects — voice stealing, global FX bus writes — the silent ones still act. Plus the green-check problem twice in one day, convergence as a bug report, and the honest limit of verdict-equality checking.
PLN (Algolia) authored -
The bug that ate PLN's whole afternoon, and the reason he was working around it by hand: "so for now hush then ctrl+enter seems like a fix ahah but not a lovely one :D". ## The symptom, and the clue that cracked it "wait i feel theres still a xfade issue? when i leave d5 on the 33 effcet, it fades in oblivion!" (and d4, d7, d8, d12...) "reboot-then-ctrl-enter: doesnt seem to obliviate, loops forever. sounds like a track-to-track state bug" Same code, two outcomes, depending on history. That is the whole diagnosis in one sentence — and `hush` curing it is the confirmation, because `streamHush` PREPENDS silence to the pattern history. ## The mechanism `BootTidal.hs:100` made every dN a 4-cycle crossfade (#13): xfade i = transition tidal True (Sound.Tidal.Transition.xfadeIn 4) i d5 = xfade 5 . (|< orbit 4) Tidal's xfadeIn is one line: xfadeIn t now (pat:pat':_) = overlay (pat |* gain rising) (pat' |* gain falling) The envelopes are correct — measured with queryArc, the incoming rises 0.442 -> 1.0 and the outgoing falls 0.990 -> 0.0 across the 4 cycles, then both saturate. The bug is that the outgoing pattern is turned DOWN but never OFF. `|*` takes structure from the LEFT, so `pat'` keeps every event it ever had. Measured: at cycle 64 of a 4-cycle fade, the outgoing pattern still emits one onset per cycle. **And a gain-0 event is not a harmless event.** SuperDirt broadcasts the cut group in `playSynths` (DirtEvent.sc:182-189) *before* amplitude is ever read — `~amp` only appears later, at line 160, as an argument to the gate synth — and for a positive `cut` it sets `cutAllSamples: 1`, releasing EVERY voice in the group regardless of sample. So the silent ghost keeps killing the incoming pattern's voice on every single hit, forever. The orbit is not fading out. It is being cut to death by its own predecessor. Every observation follows, including the ones that looked contradictory: * only orbits that own a `# cut` die. In take_5_drops, where PLN said "weirdly all d123 stay", d1/d2/d3 have NO cut group — and the ones he reported dying in vague_de_crime (d4, d5, d7) have cut 4, cut 5, cut 7. * a fresh boot is fine: one pattern in history, and `xfadeIn _ _ (pat:[]) = pat` returns it untouched, envelopes and all. * "4 bars" is the fade length. You hear the OLD pattern fade out — it wins the cut, being overlaid second and therefore sent last — and the new one never arrives at all. * d8's "weird glitches instead of proper heading breaks": cut 8, a chopped break fighting its own ghost. Not explained by this, and still open: take_5_drops' d4, which has no cut group at all. That fade is a separate lead (it shares `crushbus 41` with d7 — see the new pvlint PV011). ## The fix Keep Tidal's envelopes EXACTLY, and stop the outgoing pattern when its fade ends: xfadeCutIn t now (q:q':_) = overlay (q |* gain rising) (playFor now (now + t) q' |* gain falling) Measured, onsets per cycle (incoming/outgoing), fading at cycle 0 over 4: xfadeIn 4 1/1 1/1 1/1 1/1 | 1/1 1/1 1/1 1/1 ghost forever clutchIn 4 0/1 0/1 1/0 1/0 | 1/0 1/0 1/0 1/0 clean, never overlaps xfadeCut 4 1/1 1/1 1/1 1/1 | 1/0 1/0 1/0 1/0 clean from cycle 4 `clutchIn` is also clean — it degrades one pattern into the other so they never overlap at all — and is arguably the better transition for a rig where most orbits own a cut group. It is NOT the default here because it changes the feel of every transition on the rig, and 7 days before a gig is not when to do that. It stays available as `clutchIn`, and Tidal's original stays reachable as `xfadeLeaky` for A/B comparison. Audibly, for a cut orbit, the transition becomes: the old pattern fades out over 4 bars, the new one arrives at full exactly as its envelope reaches 1.0. Which is what PLN already describes hearing — except that now the new one arrives. ## Validation - The binding was lifted VERBATIM out of BootTidal.hs and typechecked against the type `transition` demands (`Time -> [ControlPattern] -> ControlPattern`). The stream block cannot be typechecked in place — it closes over a live `Stream` — so this is how that part of the file gets checked at all. - `tools/check-boot.sh` gains **Pass 5**, a permanent gate: it lifts `xfadeCutIn` out of the boot file and counts onsets from the outgoing pattern at cycles 4/5/8/16/64/256. Must be 0. - Mutation-tested, which is the only reason to trust it: with `playFor` removed the gate reports 6 ghost onsets and fails. Passes 1-4 all stay green on the broken version — it typechecks, it parses as Pulsar sends it, and it emits events. Counting the outgoing onsets is the only check that can see this class of bug, which is exactly why it now runs on every boot check. - All 32 blocks still parse as single statements (#79 seam intact) — the new multi-line `case` sits inside the existing `let`, so it is still one statement.PLN (Algolia) authored -
These five could not go in the previous commit because they hold PLN's own uncommitted work from this afternoon's recording session, mixed in with the role alignment. Committing them anyway, because durability beats a tidy diff: once they are in git, a stale Pulsar buffer overwriting one of them shows up as a diff instead of vanishing silently (#95, the buffer-clobber trap). ## What is HIS in this commit - **bombe_dj**: he resolved the d1 button overflow BY HAND, exactly the way #54 proposes. The three lines phase 2 had commented out with a FIXME are back, folded onto d1's own controls: the sample swap moved to a KNOB (`midiOn "^29" (# "kick:4")`) and the kick variants merged onto `^41`. That is a manual gSel, and it is better than what the FIXME asked for. - **the_revolution_will_be_sampled**: he fixed the d8 inversion himself, restoring `midiOn "^92" (ply ...)` / `midiOff "^60" (mask ...)` before any tool got there. - **vague_de_crime**: a `# cps (130/60/4)` gesture parked as a comment and another moved onto ^30; d1's commented overflow restored onto ^41. - **do_it_right**: a `dr` typo removed, a d5 gain nudged 1.3 -> 1.4, a `-- TODO: Move to d6?` note, and an extra d5 ply gesture. - **take_5_drops**: the `31` -> `21` typo in d12's `n` pattern, one `>|` -> `|>` on d5, gains adjusted. ## What is MINE The button-role alignment only — `"^NN"` token moves, per the two commits above. ## One thing worth recording about the validation bombe_dj's d7 exposed a limit of verdict-equality checking. The swap rewrote `midiOn ("^59" - "^91")` to `midiOn ("^91" - "^59")`, and subtraction is not commutative — that LOOKS like an inverted gesture. It is not: all three references in the block flipped together (the two operands and the outer/inner `midiOn` pair), so it is a consistent relabeling and the behaviour is identical with the two physical buttons exchanged. But silent-eval could not have told me either way. Both CCs seed to 0, so `0 - 0 = 0` whichever order they are in, and the default branch is untouched by construction. **Verdict equality at the seed proves nothing about behaviour under a knob press.** It is the right gate for "did this rewrite silence anything at boot" and the wrong gate for "does this control still do what it did". The second question needs pv-at (#74), or an ear.PLN (Algolia) authored
-