- 16 Aug, 2026 20 commits
-
-
PLN: 'maybe with reverb over last 5s so we hear it slightly echo as last sound'. Rendered as an A/B rather than baked in — an echo on the final sound of a record is a taste call, and 'maybe' is not a decision. Two flavours (subtle / more) plus the dry render, so the comparison is three clips in the same view. The wet part is the TAIL ONLY: a global reverb would wash the whole ending, and everything before the last 5s stays identical to dry. The tail gets apad before the echo so the ring has somewhere to go — without it ffmpeg chops at the last sample and the 'echo as last sound' becomes another hard cut, which is the thing being fixed.
PLN (Algolia) authored -
PLN (Algolia) authored
-
The release check turned up four fixes and one boundary move. The pipeline could express none of them, because it only knew about boundaries. It now separates two things that look alike and are not: `verified` a BOUNDARY: moving it shifts both neighbours (butt-joined set) `edits` a per-track TRIM/FADE: the audio is DROPPED, a gap opens, and the continuous mix must skip it too Confusing them silently hands a neighbour a second of someone else's silence — which is the exact artefact these edits exist to remove. Applied, all verified by ratio against the unfiltered master rather than by reading back the command line: #1 Bombe end -1.0s PLN: "trim silence to 1s silence from 00:17". Measured: audio ends 527.90, then 2.0s of -60..-100. #2 WAP end -45ms PLN heard "a sound just at seam ... is an error". +30ms fade The error was at WAP's END, not Drums' head: the last 40ms jump -57 -> -15 dBFS, a sound truncated mid-decay. An 80ms fade only reached -26.8 dB — hidden, not gone — so the burst is now dropped outright and the fade only guards the new edge. #12 Mafia +1.66s boundary MOVE: heard as a seam, it wants more Ghosts #13 CRIME end -10.0s PLN: "cut at 0:10 its better. have fade from 0:09 to duck to 50% 0:10 from 100 to 50% to lower the sudden cut" #15 REVOLUTION start +18.9s PLN: "start revolution at 0:38:9 on a fade of the 2s fade-in drums to not be so sudden" end -15.0s PLN: "end at 0:15" Measured gain curves confirm each: CRIME flat 1.0 then ramping to 0.546 over the last second; REVOLUTION 0.022 -> 1.0 across 2s; WAP 1.0 until the fade. Every duration exact. `continuous` was rewritten as a consequence. It used to cut merged "kept spans" from the master, which was right for boundaries and would have been silently WRONG here: the mix would have played un-faded audio while the tracks were faded, with nothing to flag it. It now cuts the same segments through the same filter chain and concatenates them, so the mix is literally the tracklist and the two cannot drift. Gaps fall out for free. PLN's raw release-check export is committed as provenance. Still open: his "maybe with reverb over last 5s so we hear it slightly echo as last sound" — supported as `reverb_tail_s`, deliberately left off pending an A/B.PLN (Algolia) authored -
PLN: "this becomes our tooling, often a set will be postprocessed as we did — reusable scripts and views plz". So the per-gig finish.sh is replaced by spec-driven tools: a new gig is now a copy of judge_specs/<gig>.json, never a copy of a script. POSTPROD.md documents the whole flow and the trap each stage exists to avoid. render_release.py — split every variant, verify durations and format, and re-render the continuous mixes. Ran clean: 14 tracks x streaming+club, all durations within tolerance, 48000/24. Its `continuous` stage answers PLN's other catch: "imo the master club rec will also have desire removed". A set is released TWICE — as tracks, and as the unbroken mix — and cutting a track had only fixed the first; both continuous masters still played Desire in full. This matters past tidiness: the continuous upload is the lower-risk rights path (an isolated track was flagged on SoundCloud where the 87-minute mix passed), so the mix is the artefact most likely to ship and it has to match the tracklist. The excision derives its kept spans from the same segments the tracks came from, so mix and tracklist cannot disagree. Both variants now render to 4403.65s = 73.4 min, matching the sum of the 14 tracks exactly: [1.45 .. 4166.30] 69.4 min + [4547.57 .. 4786.37] 4.0 min build_release_joins.py — the confirmation pass PLN asked for ("i wanna confirm all tracks start/ends/transitions"). It deliberately does NOT use the master: once Desire is dropped, the seam a listener hears (CRIME straight into REVOLUTION) exists only in the rendered output, and a master-based window there would play six minutes of a track that does not ship. Building from the split FLACs also audits the files that actually ship, so a split error surfaces before upload rather than after. 15 clips: the record's opening, 13 seams, and its closing. apply_boundaries now emits the splitter's native shape — `track` is RELEASE position because tidal_ears.master split uses it for both filename and the `track=N/total` tag; performance order stays as `perf_track`. Emitting performance order would have named the last file "15 - REVOLUTION" in a 14-track album and tagged it 15/14, which stores reject. Both UI sets pass bounds-smoke (11 assertions each). The test now derives its data file from the page's own ?set= — hardcoding it meant it could assert against a different set than the browser had loaded, passing or failing for reasons unrelated to the code.PLN (Algolia) authored -
PLN's last call: "00:31:3 is a decent end of crimewave to cut early on end of the crime sound". Read off the transport, which shows CLIP time, and #14's clip starts at 4135.0 — so master 4166.3. The audio agrees rather than merely permitting it: the crime sound gaps to -71 dB at 4164.5 and 4166.3 sits just past that on its decay, 13.7 s before the nominal 4180.0. Recorded with that mapping written out, because clip-vs-master is the one confusion that would shift a cut by 45 s and look plausible. apply_boundaries.py turns the ear file into segments_v4.json. Made a tool rather than a hand edit because this is the step that shipped a wrong album once, and its three rules are each silently destructive to get wrong: - starts come from the ear, ends from the neighbour (butt-joined), so editing one start moves two tracks; - a track CUT from the release still owns its boundary — Desire is dropped but the edge into Desire is where Vague de CRIME ends, and ignoring it would run the last official track into Desire's intro; - release position is not track number: dropping a track renumbers the album but must not renumber the provenance, so both `n` and `track` are emitted. It refuses to write on overlap, non-positive duration, out-of-master range, or an ear-verified start that failed to reach the output. All green: 14 tracks, 73.4 min kept of a 79.8 min master, 6.4 min dropped (Desire). Remaining unverified edge, for the record: #2's start (531.35) is still the nominal, so #1's end rides on it. It was never flagged in the judge pass. Ear feedback archived in performance_notes.md, including the finding that #11 and #12 disagree about which fraction is right — takeover on one, mid on the other, 8 s apart. Two adjacent cuts, opposite answers: where a cut belongs is a musical judgement about that handover, not a parameter to fit.PLN (Algolia) authored -
PLN made the four calls in the boundary lab. Merged into `verified`, so the ear-boundary file now covers #1 and #3-#13 and #15; `unjudged` is empty. The raw export is committed next to it as provenance rather than being consumed and thrown away. #11 3557.33 takeover was +0.13s -> the machine had it #12 3680.10 takeover was -8.00s -> "here the right cut is mid :)" #13 3887.25 ear-only, no candidate could exist #15 4547.57 ear-only, no candidate could exist Worth recording that #11 and #12 disagree about WHICH fraction is right: on one the takeover was the cut, on the other the midpoint was, 8 s earlier. That kills any remaining hope of a single universal fraction and confirms the tool's shape was the right call — offer the candidates, let the ear pick, do not average. The unnoticed gap: #13's END. The set is butt-joined, so that edge is the boundary INTO Desire (nominal 4180.0), which was never verified because Desire itself is cut from the release — it looked like work that no longer mattered. It is the opposite: with Desire dropped, that edge is where the last official track ends, and if it is late then Desire's intro bleeds into the end of the record. Rendered it as a fifth boundary; the builder now reads `cut_from_release` and labels such an edge "END of #13 Vague de CRIME" with an on-screen explanation, so the next gig cannot lose the same edge the same way. Also made bounds-smoke pick its subject by content instead of position: it broke the moment #11 became settled and lost its candidate buttons, which is a data change, not a regression. It now prefers a boundary that still has a machine candidate and falls back to the nominal marker every boundary carries. 11/11 against the production server.
PLN (Algolia) authored -
PLN (Algolia) authored
-
The set-judge answers "is this track a keeper". This answers the question that actually blocked the release: where exactly does one track become the next. After three failed detectors (cut_lens3 declared a dead end in 5f7db02a), #13 and #15 are ear-only by nature — the two tracks share their sounds across the switch, so no timbral lens can see the handover. The tool's job is therefore to make one ear call cheap, not to avoid asking. build_bounds_set.py renders a ±45 s FLAC clip per boundary instead of seeking the 817 MB master. A boundary audition is a scrub — dozens of small seeks around one point — and seeking a long FLAC depends on a seektable the browser may not use well; local clips make every seek instant for a few MB total. Clips are re-encoded, never `-c copy`, because FLAC stream-copy snaps to frame boundaries and every marker inside the clip would inherit that offset. Clip time and master time are kept explicitly apart (`clipStart`), converted in exactly two functions. The UI shows each cut's candidates as labelled markers on the waveform, drawn as regions rather than positioned divs so they stay glued to the audio through zoom and scroll. `takeover` leads the list because it landed within 0.7 s on both boundaries that also had ear answers — with the caveat printed on screen, since that is n=2 on the cases that were already easy. Boundaries with NO candidate are shown with an explicit notice rather than omitted: those are precisely the ones needing a human, and hiding them would hide the work. Fixes a real bug in the SHARED player while here: `ws.zoom()` needs a decoded buffer, which the peaks path never has, so it threw "No audio loaded" from an effect and blanked the entire page. Same message as the peaks+url trap, entirely different cause. Now guarded and failed-soft — zoom is a convenience and must never take the tool down. The judge had this latent too. bounds-smoke.mjs, 11 assertions, all passing. Two of them exist because the first version of this test passed while proving nothing: querySelector('audio') is null by construction (wavesurfer owns the element and never puts it in the DOM), so the playback probe would have "passed" forever on a dead transport. It now reads the transport clock and the Pause button — what a human sees. A positional selector also silently picked the header's counter instead of the clock, hence the explicit data-testid.
PLN (Algolia) authored -
Two changes to the ear-boundary ground truth, kept deliberately distinct. #1 was the last GO with an unfinished instruction — PLN said "trim leading silence tho" without an amount. The amount is now measured rather than estimated: every sample is EXACTLY zero up to sample 64038 (1.452109 s @ 44.1 kHz), and the first audible frame peaks at -28 dBFS. That is a sample-exact scan, not a threshold and not a detector, which is why it is allowed into a file whose header forbids detector output. Recorded as start=1.45, keeping a ~2 ms guard so the first transient cannot be clipped, with the full measurement and method stored alongside it for audit. #11 and #12 gain machine PROPOSALS in a new `candidates` block, structurally separate from `verified` so nothing can be mistaken for a confirmed boundary. On the two boundaries that also have ear answers, the `takeover` estimate landed within 0.7 s (#8 2726.25 vs 2726.4, #10 3268.3 vs 3269.0) — which is a reason to audition takeover first, not a reason to trust it: n=2, on the cases that were already easy. #13 and #15 are listed with an explicit NO CANDIDATE and the reason, so a future reader does not go looking for numbers that cannot exist: zero voting orbits across three gates and two estimators, because those transitions share their sounds and a timbral lens is blind to them by construction.
PLN (Algolia) authored -
The Claude-Code Task API is not reachable from this session (TaskList/TaskCreate/ TaskUpdate/TaskGet all absent), so the live board — last seen at 112 tasks — could not be read or rewritten. Rebuilt the tree from durable sources instead: board-archive.md (ids exact, completed through 2026-07-29), the numbered achievement logs, the memory store, and git. Structured as six epics with the work nested underneath: OPAL-26 release (the only one blocking a shipment), cut/boundary detection, the floor problem, rig & hardware, open taste calls, completed. Each item carries the measurement or the quote that justifies it rather than a bare title, so it survives a cold read. Honest about provenance: ids in the #150+ range come from the lost board via session context and cannot be re-verified, so the file says to treat a #17x number as a name, not a key. This session's outcomes are slotted in place: #1's leading silence measured (1.452s), the timbral cut lens declared a dead end with its three-attempt table, and #13/#15 escalated to ear-only calls.
PLN (Algolia) authored -
The medoid + effect-size gate was meant to buy coverage after run 1 returned "no separating orbit" for six of fourteen boundaries. It did the opposite: boundaries with any voting orbit went 8/14 -> 4/14. Requiring cross-side distance to beat within-side spread is a stricter test, not a better-aimed one. The error table is seductive and is not evidence: u=0.85 scores median 0.4s, worst 0.7s — but n=2, and those two boundaries (#8, #10) already had ear answers. Four survivors cannot validate five candidate fractions, and a metric computed on the cases that were already easy measures nothing. The durable finding is the pattern across all three attempts: #13 Vague de CRIME and #15 REVOLUTION have produced ZERO voting orbits in v2, v3 run 1 and v3 run 2 — three gates, two estimators. That is not a tuning failure. Those transitions have no orbit whose timbre differs materially across the switch; the outgoing and incoming tracks share their sounds, so a timbral lens is structurally blind there just as an activity lens is blind to a crossfade. A fourth loosening can only admit noise, because there is no signal in this feature space. Declared a dead end in the docstring rather than tuned again. Two ways out are recorded: change the feature (harmonic/key change, or sample-bank identity from the score, which differs even when the spectrum does not), or accept #13/#15 as ear-only calls — two timestamps, seconds of listening each. Candidates that DID survive are kept in cut_candidates_v3run2.json for #8, #10, #11, #12 as ear-confirmation inputs, not as boundaries.
PLN (Algolia) authored -
v3 run 1 returned "no separating orbit" for six of fourteen boundaries, including #13 and #15, the two the run existed to answer. Diagnosis was that the gate compared ONE profile averaged over 40 s per side: a mean of band percentages is a sound that never occurred, and 40 s of a livecoded track averages toward the same grey for every orbit, so the orbits that actually change look uninformative. This replaces that with a distribution test: - reference windows are framed (3 s / 1.5 s hop) instead of averaged - each side is summarised by its MEDOID — an actual moment of the track — plus the median within-side distance, i.e. how varied that side is - an orbit votes only if it differs MORE across the switch than within either side (effect = sep - mean spread), not merely by a fixed distance - REF_MAX 40s -> 24s, so a reference stays one sound NOT YET VALIDATED. Syntax-checked and smoke-run on boundary #11 only, which still draws 3 voters and moves only its u0.15 estimate (3553.7 -> 3515.7); whether #13/#15 recover needs the full 14-boundary run (~10 min) that has not been done. Committed rather than parked so the reasoning is not lost — the docstring's RESULT block still describes run 1 and must be rewritten with run 2's numbers before any boundary is taken from this.PLN (Algolia) authored -
v2 left a diagnosis, not a fix: every error was negative, and the accurate boundaries were exactly the ones PLN called short. The lens finds where the incoming track FIRST APPEARS; the ear marks where it TAKES OVER. Those differ by the crossfade length, which is the varying quantity (2-41s on this set), so a point estimator is the wrong shape of answer. v3 reports a span. The part worth keeping is that the "fraction through the fade" needs no tuning: evaluating the timbral series d(t) on the reference windows themselves gives d(ref_prev) = -sep and d(ref_next) = +sep, so u = (d + sep) / 2sep is calibrated by construction — u=0 is "identical to before the switch", u=1 "identical to after", comparable across orbits and boundaries. Every previous version thresholded a raw distance whose scale differed per orbit, which is why no threshold ever meant the same thing twice. The model behaves as predicted: u=0.15 sits 48s early (onset, confirming the v2 diagnosis), and the error finally changes sign at u=0.7 (+1.3s mean, median 3.7s, vs v2's 16.6s and uniformly negative). But it does NOT ship a number. Six of fourteen boundaries returned "no separating orbit" — including #13 and #15, the two the run existed to answer — leaving n=2 truth points against five candidate fractions. Coverage, not accuracy, is now the bug: MIN_SEP gates on the distance between two 40s MEAN profiles, and averaging that long over a livecoded track washes out precisely the orbits that change. Next attempt gates on the reference windows' frame-level distributions instead. Full result and reasoning in the module docstring. Usable today as candidates only: #11 clusters inside 3.5s (short fade, nominal ~4s late), #12 spreads over 32s (long fade, nominal mid-span).
PLN (Algolia) authored -
Archive entry for the set-judge build. The entry worth reading later is not the tool but its first run: it was built to collect go/no-go verdicts and what it actually established is that the OPAL-26 masters are fine and every middle CUT is wrong — nine boundaries ear-verified in one sitting, with errors of 3 to 21 seconds that two automated detectors had certified clean. Also records the four silent traps the build walked into (heads are intros; a green build with dead playback; three verification probes that tested nothing; two latent bugs surfaced by reusing existing components).
PLN (Algolia) authored -
Second attempt at locating crossfaded track boundaries without PLN's ears, with the three faults of v1 fixed: references taken ADJACENT to the boundary instead of from a track midpoint (a livecoded track is not stationary), a MEDIAN over every separating orbit instead of a weighted mean over 1-5 (one liar cannot move a median), and a CUSUM change-point instead of a threshold plus a 6s sustained- crossing rule that could not resolve a 4-cycle crossfade (~7.7s at 124 BPM). It got better: median error 24.0s -> 16.6s, voters 1-5 -> 4-8. It is still not good enough to ship, and I did not ship it. The useful part is the SHAPE of the failure. Every single error is negative, and the accurate boundaries are exactly the ones PLN described as short: #3 -21.0 #4 -15.2 #5 -18.0 #6 -18.5 #7 -41.2 (his: 'still the drops') #8 -4.4 #9 -2.0 #10 -2.4 (his: 'perfect starts perfect') The lens finds where the incoming track FIRST APPEARS; the ear marks where it TAKES OVER. They differ by the length of the crossfade, which is precisely the quantity that varies. So the lens is measuring a real thing, just not the one we asked for — a much better position than 'it is noisy'. Two ways forward are written into the file, plus an explicit warning not to paper over it with a constant offset fitted to these eight points: the errors span 2.0-41.2s, so a fitted constant would be memorisation, not a method. Prior art checked at PLN's suggestion and worth recording: the OPAL gig log is useless here (2 track events in 87 minutes — the gig-log 'track' field only started recording properly in 97b22c0b, AFTER the gig), but Prod/Montreuil26_master/boundaries_ground_truth.json holds 14 user-confirmed boundaries from a RELEASED split. That is a second, independent labelled set the next attempt should validate against before touching OPAL.PLN (Algolia) authored -
PLN, mid-pass: 'make the judge header fixed so decision/key/stats is easy to read always, and button to DL is always there'. Fair — the list is 15 tracks and the focused row expands to several hundred pixels, so the tally, the keymap and Export were all scrolled off exactly when they are being used. DECISION, confirmed: 'i confirm skip desire stronger set without'. #14 Desire is cut. #13 Vague de CRIME becomes the last OFFICIAL track and #15 REVOLUTION stays the Encore — which is already its section in tracks.json, so the set's shape holds. The release is 14 tracks. Two more boundaries from his relisten, now recorded as ground truth: #1 GO, but 'trim leading silence tho' — head trim needed, amount unmeasured #8 2726.4 — 'piment until 0:02, xfade starts, but unfortunate midi events make a decent eh ouais je funk single start only at 06.4'. He also floats xfading it ourselves from 0:10.9 in postprod, kept as alt_start. Nine of fourteen boundaries are now ear-verified; #11, #12, #13, #15 remain.PLN (Algolia) authored -
PLN relayed the gig's main criticism: 'destructure. les gens etaient un peu perdu' — individually good tracks, overall confusing. And his own read of it: 'I DIDNT KEEP THE KICK LONG ENOUGH.' Measured against the stems, the floor and the data say the same thing. Kick (d1) audible 59% of the set; 13 of 15 tracks under 70%; a 164-second kickless stretch inside 'Am i Doing it Right' and 116s inside 'Piment Bresilien'. The set runs 89-166 BPM, so anything built on this must count BARS, not seconds. Also logged, from the REVOLUTION listen: his first guess was wrong and his second was right. Re-ranking the orbits in the band a laptop actually reproduces (400Hz-8k) puts the_revolution (d5) on top by 13.7 dB, while like_sugar:21 (d10) — the one he wanted cut — sits 14.5 dB BELOW it and is quieter on a laptop than it is full-range. On a small speaker the loudest thing is the one that SURVIVES the band, not the one with the highest fader. And a defect nobody was looking for: d9 SUGAR CHOIR is digital silence for the whole of REVOLUTION though the score declares it, confirmed by two independent measurements. That one is why he now wants the HUD to shout about declared-but- never-played orbits until they sound.
PLN (Algolia) authored -
First real use of the set-judge, and it did its job: it found that the PROBLEM IS NOT THE MASTER, IT IS THE CUTS. PLN judged through #10 and stopped — "listened to 7, then errors seem to compound". #3 893.0 WAP bass bleeds in, real start 0:03, and the END misses a note #4 1118.4 4s too much at the head; Sunshine begins at 7:15 INSIDE it #5 1549.4 missing its own head (it is sitting inside #4); ends abrupt #6 1808.1 "still some sunshine bass here :(" — real start 0:10 #7 2248.8 "this start is still the drops" — 21s of the previous track #9 2954.4 correct: "perfect starts perfect :lol:" #10 3269.0 "starts on a perfect sound lol", real Gimme Acid at 0:16.7 Magnitude grows through the middle of the set and the direction flips, so it is not clock drift — it is per-boundary detection error. PLN named the mechanism himself: "check careful youll see the xfade". Every dN is a 4-cycle crossfade, so across a switch the ORBIT SET barely changes (d1 is a kick before and after) and every activity-based detector sees continuity. What changes is the SAMPLE. TWO INSTRUMENTS FAILED, AND BOTH REPORTED SUCCESS. `tidal-ears master cuts verify` said 13/15 PASS. It genuinely catches the stutter shape — on #3 and #4 it agreed with his ear to within a second — and it passed #5, #6 and #7 where the errors were 9, 10 and 21 seconds. So I wrote cut_lens.py to measure TIMBRE rather than activity, with a --truth harness to check it against the five boundaries his ear had already settled. It missed by a MEDIAN OF 24 SECONDS, worst 40 s, with only 1-5 orbits voting. Discarded. Committed anyway, with the negative result and three concrete hypotheses at the top of the file, because the harness is the valuable part and the next person to have this idea deserves to see the result first. The ear-verified boundaries are now DATA (judge_specs/opal26_boundaries_ear.json), marked do-not-overwrite-with-a-detector. Also fixed the first version of cut_lens spawning ffmpeg once per window — 14 boundaries x 12 orbits x ~90 windows = 15k execve for four seconds of arithmetic. Decode the span once, slice in numpy. FEATURE, from "players need to autoplay next so we can see how it goes into ;)": the judge now chains into the next track by default. A per-track list otherwise makes transitions the one thing you cannot hear — which is exactly what this whole pass turned out to be about. Manual j/k navigation never auto-plays, so moving around stays quiet; the toggle is in the header. All his notes archived verbatim in performance_notes.md with the implied absolute boundary for each, per the archivist rule. Validated: 11/11 smoke green against the built dist; tsc clean.PLN (Algolia) authored -
PLN: "make me a tiny spa to see the heads, soundwaves and metadata of what dX are running at that moment / what samples/synth they are, so i can go/nogo / comment each, then download decisions.json. its a reusable tool tbh we do this flow often." That flow was being run in a music player with the answers landing in a chat log, so three weeks later nobody could say why a track was cut. Ear-feedback is the scarcest input in this pipeline and it was the only one with no artifact. Built on the existing A/B Judge rather than beside it: OrbitRail now takes binS + orbits instead of a whole Take, so one rail serves both UIs. Shapes live in models.py and the TS is regenerated from them, as before. FIFTEEN 16-SECOND HEADS ARE FIFTEEN INTROS. The first build showed 1-4 orbits per track and I nearly shipped that as a finding. The heads are bit-exact with each track's first 16s (r=1.0000 by correlation against the full track), so the sparse lanes were TRUE and completely misleading — these tracks run 7-10 orbits once they open. A ship/cut call on an intro is a call on the wrong evidence. Activity now spans the whole track; the head stays as a running-order scan, with a banner saying exactly what it is. PEAKS ARE THE FEATURE, NOT AN OPTIMISATION. Without precomputed peaks wavesurfer downloads and decodes the entire file before drawing one pixel — 102 MB per track here. Blank screen at the desk, unusable over wifi on a phone, which is where the mono-compat check happens. But passing `peaks` + `url` together makes wavesurfer treat the track as pre-decoded and never wire up playback: waveform draws, transport looks alive, play throws "No audio loaded". Both `tsc -b` and `vite build` were green through all of that. Fixed by owning the <audio> element via `media:`. THREE VERIFICATION PROBES IN A ROW TESTED NOTHING, each green or red for reasons unrelated to the app: 1. fetch(document.querySelector('audio').src) asserting 206 — there was no <audio> in the light DOM, so it fetched the empty string, got THE PAGE, and read a 206 from somewhere else. It passed. 2. querySelectorAll('li canvas') — wavesurfer renders into a SHADOW ROOT. Playwright's selectors pierce it; querySelector inside evaluate does not. It failed while the waveform drew perfectly. 3. A tally assertion matching "2 judged" against text reading "2/15 judged". The probe that works asserts what PLN can see: the clock advances and the button flips to Pause. Prefer the assertion a human could make by looking. Two bugs fell out of reusing existing parts, which is the argument for reusing existing parts: - classify_family filed vec1_claps, drumtraks, realclaps, clubkick and 808bd under MELODIC — it matched perc names by exact-match or prefix, and in these scores the perc word is a suffix or an underscore token. A rail that shows claps as melody is worse than no rail. Widened to suffix + token matching; the test pins that cpluck, dropbass and snippet still must NOT match, since those collisions are why exact-matching existed. - mmss() padded a spurious zero onto every sub-10s time ("0:002.2"), off by one since it was written, in the shared helper. Caught by the smoke test reading the clock back. Titles carried markdown into the release path: tracks.json holds "There's **Something About Drums**" because backlog.md is prose, and those strings flow decisions.json -> upload metadata. Emphasis stripped; "<3" and the shouting caps are PLN's and stay. Spec-driven so the next gig is a copy of a JSON file, not a code change. The master->stem map is explicit numbers (keeps summing to 4786.370s, the master's duration to the millisecond) and the builder asserts it rather than trusting it. audio-mounts.json is read by BOTH vite's dev middleware and serve.py, so a URL that works in dev works on the phone over LAN. Validated: 11/11 smoke green in dev AND against serve.py on the built dist; 22 classifier tests; path traversal out of an audio mount returns 404.PLN (Algolia) authored -
PLN got "memory shortage avoided — parvagues-sc terminated because the system is low on memory". Nothing in the system had decided to kill it: systemd-oomd is disabled and has NEVER run, earlyoom/nohang inactive, and the kernel OOM killer left no trace in `journalctl -k` or `-b -1`. The string "memory shortage" appears in no journal at all, so the notification came from an app — source still unidentified, and currently the only warning that exists. THE MECHANISM — a stale binding, this rig's most expensive recurring shape. perf.sh:545 protects the rig BY PID (chrt -f 90, oom_score_adj -1000). systemd's user manager ships DefaultOOMScoreAdjust=200, so EVERY restart of parvagues-sc — crash, watchdog, or manual — hands scsynth back at +200, the most attractive OOM victim on the machine, with no realtime priority either. perf.sh is correct exactly once. On 2026-08-15 SC restarted 6+ times (four SIGSEGVs, e65e3a87) and spent the rest of the session naked. The tell: live processes read -1000 while `systemctl show` still says 200 — that gap IS the bug. WHY A SYSTEM UNIT. Measured, because it is easy to assume wrongly: OOMScoreAdjust=-1000 -> 100 OOMScoreAdjust=0 -> 100 OOMScoreAdjust=-500 -> 100 OOMScoreAdjust=200 -> 200 A --user unit CANNOT go below 100 and it clamps SILENTLY — no error, no log. Writing -1000 into parvagues-sc.service would look right in the file, look right in `systemctl show`, and do nothing. Lowering past the manager's own value needs CAP_SYS_RESOURCE. PLN: "make it system if useful ... this device is parvagues-pilled" / "i can sudo any good shit". WHAT SHIPPED tools/parvagues-protect.sh 2s sweep; only ever lowers oom, only ever raises priority, so it is idempotent and composes with perf.sh instead of fighting it. Logs ONLY changes — at 2s, "still fine" would be 43k journal lines a day, i.e. no logging. --check is read-only and unprivileged. tools/parvagues-protect.service root, hardened (three capabilities, no more), Nice=10 + IOSchedulingClass=idle so the protector is never itself a cost. tools/install-protect.sh COPIES the script to /usr/local/bin root:root 0755 rather than running it from the repo — a root unit with ExecStart under /home/pln is a privesc hole, unlike the user-owned watchdog. tools/gig-up.sh new SOFT check "SC un-killable". SOFT is a judgement, not an oversight: an unprotected rig still makes sound, and this gate's own rule is that HARD means "there is no gig". It never starts or stops anything — that stays the watchdog's job, and two supervisors with opinions about one process is a fight nobody wins. TWO BUGS FOUND WHILE BUILDING IT - `pgrep -f ardour` reported Ardour RUNNING when it was closed: it matched `bash .../tidal-ardour-autoroute.sh`, which has "ardour" in its path. I had already repeated that false positive to PLN as "Ardour up but engine not started". Now matches comm exactly, and --check PRINTS absent targets rather than skipping them silently. - `tools/check-audio-graph.sh` was +x on disk but git recorded 100644, failing the gate's own "tools executable" check. Fixed with update-index --chmod=+x (feedback_chmod_is_not_a_fix: chmod fixes your tree, not the repo). VERIFIED, both directions, because a check that cannot fail is decoration: --check returns 0 on the live protected rig, and against a decoy process at oom_score_adj=200/SCHED_OTHER it prints UNPROTECTED and exits 1 — that decoy signature is exactly what a freshly restarted scsynth looks like. NOT YET PROVEN: recovery after a real restart, which needs the unit installed (root). install-protect.sh ends by printing that verification.
PLN (Algolia) authored
-
- 15 Aug, 2026 3 commits
-
-
PLN, mid-session in the cafe tracks: "some d1 had still 42 not 41 as button, did we not do all refact? doesnt our tools shout at such error snow?" Both halves were right, and the answers are different. DID WE NOT DO ALL REFACT — no. CC 41 used to be gMask, so while it was, an orbit's gate started a column late: d1 -> ^42, d2 -> ^43. Retiring gMask freed 41 and REMAP PHASE 2 (5910aacc, 2026-07-29) column-aligned the board. That commit touched FOURTEEN FILES — the OPAL set, not the corpus. 174 of 696 tracks in live/ still carry the old layout, cafe_glace / cafe_tiede / cafe_bouillant among them. Measured: every single shifted file is +1. Not 174 typos, one layout generation. DOESNT OUR TOOLS SHOUT — they structurally cannot. pvlint's button rule is PV008, "one physical button driving two orbits", and it fires on a COLLISION. A uniformly shifted file has no collision: every orbit is off by the same amount, so no two orbits ever meet on the same button. The file slides one column over and stays internally consistent. PV008 is blind to it by construction, and it is the rule that caught the perfect.tidal mis-migration — so this is a genuine gap, not a missing check someone forgot to run. WHY THIS IS NOT A NEW PVLINT RULE PV014 set the house bar: measure the convention before encoding it (it earned its place at 894/984 = 90.9%). The same measurement for buttons: button refs on orbits 1-8 2700 on the orbit's OWN column 963 (35.7%) on ANOTHER column 1737 (64.3%) 35.7% is a minority, not a convention. A strict "column N = orbit N" rule would emit 1737 findings and hold the pre-gig gate shut over the corpus's normal state. The crisp signal is the shift SIGNATURE — >=3 orbits sharing one non-zero offset — which is one finding per FILE (174) instead of one per reference (1737). So this ships as a standalone lens, not a gate rule: it cannot break gig-up.sh while PLN is away, and severity stays his call. Migration is migrate-columns.py's job and is POST-GIG on purpose: retuning 174 tracks' muscle memory days before a set is how you reach for the wrong button on stage.
PLN (Algolia) authored -
Sudden silence while changing track, an "audio server died" notification, then crackle and no sound while Ardour's faders still moved. Four scsynth SIGSEGVs in four minutes (14:01:58, 14:02:55, 14:03:52, 14:04:53), each ~57s apart, until the watchdog hit its bound: "GIVING UP: 3 restarts in 600s and scsynth will not stay up." sclang stayed alive throughout, so the systemd unit read active (running) the entire time — green unit, no sound, exactly the supervision trap. WHERE IT DIED Every coredump carries the same frame #1: JoshPVUGens.so, on the audio callback thread (libpipewire -> libspa -> libjack -> libscsynth -> JoshPVUGens). The only route into that library from this rig is SuperDirt's `spectral-freeze` module in default-effects-extra.scd, i.e. Tidal's `# freeze`. Eight live tracks carry it, including cafe_glace.tidal:44 — the track being played when it started. WHY IT DIED The server was starving before it fell over: `LocalBuf_allocBuffer: alloc failed`, 18x `Buffer UGen: no buffer data`, and `late` values of 6-13 SECONDS. SuperDirt builds one spectral-freeze synth PER EVENT, each allocating FFT(LocalBuf(2048)) per channel, so a dense pattern under `# freeze` drains the RT pool even at the configured memSize of 256 MB. The difference that matters, measured in NRT with 120 overlapping synths and a starved pool (both survive non-realtime, but their behaviour diverges): PV_Freeze LocalBuf_allocBuffer: alloc failed <- and then nothing PV_MagFreeze PV_MagFreeze_next: alloc failed x6 <- detects it, bails The core UGen guards its buffer. Josh's does not — it proceeds with the failed allocation. Single-threaded NRT survives that; the realtime callback does not. THE FIX Re-`.add` "spectral-freeze2" after ~dirt.start, identical but for PV_MagFreeze — the core-SC sibling JoshUGens' own help file compares itself to, with the same trigger semantics (freeze > 0 freezes magnitudes). Slightly more static, since PV_Freeze also advances phase between frames; in exchange it cannot take the server down. Verified live: boot prints "[boot] spectral-freeze overridden", scsynth stable, 96 SC->Ardour links re-made by the autoroute unit. Pre-existing and NOT caused by this change: `SynthDef global_mi_verb2 not found` appears from 14:02:31, in a boot that predates the edit. Filed separately.PLN (Algolia) authored -
PLN: "i wanna floor youtube with individual clips for each good rec we have". The renderer could not do that: it had exactly ONE output shape, 1080x1920 vertical, and no way to render a whole track. WHY IT WAS STUCK AT ONE SHAPE The geometry was hardcoded in three separate places -- the Playwright viewport, the CDP screencast cap, and the ffmpeg -vf. Adding a shape meant finding all three and keeping them consistent forever, so nobody did. They now derive from one SHAPES table: vertical 1080x1920, square 1080x1080, landscape 1920x1080. The viewport is the load-bearing one and the reason this is not just a crop. Hydra COMPOSES for the frame it is given, so the shape has to be chosen before the scene renders -- a 16:9 scene centre-cropped to 9:16 throws away most of the motion, and the reverse is equally true. The viewport is now output/dpr, which at the default dpr 2 is 540x960 for vertical: byte-identical to the old behaviour, so existing renders are unchanged. FULL-LENGTH `--dur full` (ad-hoc) and `--cut full` (idea path) render start-to-end, with the length measured by ffprobe rather than assumed. `--cut full` stays on the idea path deliberately so a YouTube cut still inherits that idea's playsets and fx instead of silently falling back to the ad-hoc defaults. THE BUG THIS WOULD HAVE CAUSED, AVOIDED The output filename now carries the shape. Without that, rendering square, vertical and landscape of one cut writes the same path three times and leaves only the last -- a batch that quietly produces a third of what it claims. An unknown --shape throws at startup, before a multi-minute realtime capture, not after it. VALIDATED END TO END node --check clean vertical -> viewport 540x960 identical to before --shape hexagon throws at line 80, pre-capture --dur full on a 446.777729s file planned "+446.8s" 15s square test render 1080x1080 h264 30fps, aac 48k NOTE for anyone running this: the playwright browser cache on this box is EMPTY, so it needs SLOP_CHROME=/usr/bin/chromium (the script already supports it) plus the hexa dev server on :5173 under node 22. Capture is realtime, so a full render costs the track's duration PER SHAPE.
PLN (Algolia) authored
-
- 14 Aug, 2026 17 commits
-
-
#150, and the fix at the source of #148. gig-log recorded tab-focus events -- which file PLN was LOOKING at. Album track boundaries were then derived from those, and they were wrong, because a livecoder tabs around constantly while a pattern keeps running. Looking at a file is not playing it. An EVAL is the moment a pattern actually changes. That is the honest boundary. SIGNAL SOURCE `atom.commands.onDidDispatch` in the HUD package, filtered to `tidalcycles:eval*`. Confirmed ctrl+enter maps to `tidalcycles:eval-multi-line` in Pulsar's own keymap, and that MULTI_LINE sends exactly the blank-line-delimited paragraph around the cursor -- which is already this repo's block convention. onDidDispatch rides Atom's EXISTING command routing, so it adds no new per-keystroke listener: it cannot make the renderer burn worse. Rejected: GHCi stdout (no stable per-eval marker without changing the REPL protocol) and SuperDirt OSC sniffing (needs its own listener process -- closer to the observer-perturbs trap this rig has already been burned by). SHAPE New `k:"eval"` record {t, path, d}, where d is a best-effort list of dN streams found by a narrow regex: dN as the FIRST token of a line, inside the evaluated block only. Documented as best-effort rather than dressed up as complete. Transport is ~/.cache/parvagues/eval-events.jsonl, APPEND-only -- unlike current-track, which overwrites. An eval is an EVENT, in the same category as a MIDI note, never coalesced; current-track is STATE. Conflating the two is how the focus lens got mistaken for a play lens in the first place. `EvalTail` polls it once per tick with a byte-offset seek, buffers torn lines, and rebaselines if the file shrinks or is recreated -- the same pattern XrunReader already uses for node respawns. It never replays pre-session content. `track` events are UNTOUCHED. This adds a lens; it does not replace one. Boundary detection wants several (gap < orbit-flip < tempo), and focus is still the cheapest of them. BLAST RADIUS Folded into load()'s existing marks list exactly as `track` is, so cmd_report renders it for free and load()'s tuple signature does not change. The one real consumer, tools/take-segments.py, reads the JSONL itself and already ignores any `k` it does not recognise -- verified by reading it, not assumed. VALIDATION 111/111 python tests pass (91 pre-existing + 20 new), covering the pure normalizer, the tail's offset / torn-line / rebaseline behaviour, and a full write -> load round trip. NOT VERIFIED: whether the running gig-log.service picks this up without a restart. It was deliberately left alone -- PLN was performing.PLN (Algolia) authored -
PLN, mid-session: "why atm i see no midi color on lcxl reacting to my touches?" The LED watcher was enabled, active, 6h uptime, re-parsing the loaded track into the journal every few minutes. `systemctl status` was green. And it had decoded exactly zero MIDI events the entire time, because its aseqdump child was bound to `20:0` while the LCXL had moved to `28:0`. WHY THE EXISTING DEFENCE DIDN'T FIRE cmd_watch already re-resolved the port by name on every respawn -- the comment on that line even says so, and it is true. It is also useless, because respawn is triggered by the child EXITING, and an aseqdump whose sequencer client has vanished does not exit. It blocks on a dead subscription indefinitely. The read loop never ends, so the re-resolution is never reached. A re-resolution that is never reached is indistinguishable from a correct one. Same shape as the two lenses that shipped with thresholds that could never fire: the code was right, the trigger was wrong. THE FIX Detect the drift from OUTSIDE the read loop. A 10s poll compares the live name resolution against what the current child is actually bound to; on a change it invalidates the send-side port cache and terminates the child, and the existing main loop respawns and re-resolves through the path it already had. No new resolution logic -- find_seq_port() was correct all along and returns 28:0 today. Only the re-check was missing. Deliberately NOT keyed on "no events for N seconds". A board nobody is touching is also silent, so that check would fire through every quiet passage and respawn the tap mid-set. Port identity changes when the binding is genuinely stale and at no other time. VALIDATION find_seq_port() -> 28:0 (resolution was never the bug) before restart: aseqdump -p 20:0 6h stale, 0 events decoded after restart: aseqdump -p 28:0 re-parsed rose_rouge -> 26 controls The restart also reaped its own orphan. Two unrelated zombie taps remain (6.4h on the dead 20:0, 7.7h with no -p at all) -- that is #129/#155, logged separately, not addressed here. NOT PROVEN: the drift watchdog itself has not fired yet, because that needs the port to actually move. Unplug and replug the LCXL and the journal should show "LCXL moved 28:0 -> NN:0; respawning the tap". Until someone sees that line, this fix is reasoned, not demonstrated.
PLN (Algolia) authored -
The OPAL tracks.json on the website still carried the pre-#148 tab-focus boundaries, drifting up to 50 s from the measured cuts — and that file feeds both the site and Slopmotion, so the visualist would have been cutting to timecodes that no longer describe the audio. Regenerating it should have been one command, except the two sides of the pipeline describe a segment differently: {name, start_s, end_s} what this tool was built for — keyed by .tidal stem {title, start, end} what the mastering side produces (segments_v3.json) Rather than add a converter script beside it, the tool now accepts both. One parser per concept: "a segments file" is one idea and should have one reader, not a reader plus a shim that can disagree with it. The v3 shape is matched to the setlist BY TITLE, never by position. Position would work today and silently pair the wrong boundary with the wrong track the first time a soundcheck row or an unplayed track shifts the list — and this is exactly the class of bug #148 already was. An unmatched title is therefore a hard failure that prints both sides, not a warning. That strictness paid for itself on the first run. Two titles failed to match: "There's Something About Drums" vs "There's **Something About Drums** <3" "Perfect" vs "Perfect <3" backlog.md is prose written for a human, so names carry markdown emphasis and hearts. The fold strips punctuation but KEEPS DIGITS — so `<3` survived as a trailing `3` and `Perfect` folded to "perfect3", matching nothing. Stripping `<3` before the alphanumeric fold is the fix, and the ordering of those two steps is the whole bug. Validated by diffing the regenerated file against the canonical one field by field: names, order, bpmRange, styleDistribution and all 60 samplePacks are byte-identical; only start/end/duration moved, on 13-15 of 15 tracks, plus totalDuration_s 4809 -> 4769.1. Worst drift Perfect +50 s, then Gimme Acid +33 s and Ghosts +32 s. venue and stage are passed through verbatim — omitting the flags drops them, which the diff caught before anything was written.PLN (Algolia) authored -
The evening the ALSA client numbers swapped and both of the morning's fixes became wrong at once — while the one tap I had written up as broken turned out to be the only correct one. Covers the name-resolved surface tap (#155), the preference-list output router (#41), the scene keys that could never fire and the one that muted d10 (#134), and the hush that threw a modal dialog over the editor on a dormant REPL. The learning is the title: every fix I shipped that morning wrote down a port number, which looked like a fact and was a lease. Also the smaller trap that cost twenty minutes — with the surface unplugged, the discovery commands list no hardware at all, so I nearly 'fixed' a tool that was working perfectly.
PLN (Algolia) authored -
An evening's live editing on the track, committed so it is not left sitting in a buffer overnight. Net +38/-16 across the twelve orbits. d6 NEW — "Jorja too, she wants _you_": a second rose voice, masked to "<f f f t>" so it answers d5 rather than crowding it d7 the vocal-chop gate moves to ^35 (HANDS IN THE AIR) and the phrase end lands at 0.55, one notch past the "i want you to get" cut d8 the break gets loopAt 2 inside both midiOn branches, and the Orleans 8/8 variant replaces the old n 22 Recorded here because the sampling table in the comments is the score for this track — the `end` values ARE the lyrics, and re-deriving them by ear is an evening's work. Still standing, from earlier today (#156): d4 and d11 carry bare "^NN" inside a `#` (crushbus on ^35, delay on ^19). Those only emit because BootTidal's boot seed gives every CC a value — silent-eval with an EMPTY map still reports both orbits SILENT, which is the honest cold reading, not a regression.PLN (Algolia) authored -
Two pieces of the cockpit (#155, #41), both written the same way: resolve by IDENTITY every time, never cache an address. tools/bridge/surface.py — THE control-surface tap ------------------------------------------------- Four tools each spawned their own aseqdump. This morning three of them were bound to ALSA client 24 (an Arturia KeyStep) instead of 20 (the Launch Control XL) because a replug had renumbered the clients, and each resolves its port once, at start. I "fixed" them by moving them to 20:0. Then PLN replugged again this evening and the numbers SWAPPED BACK: client 20: 'Arturia KeyStep 32' <- was the LCXL client 24: 'Launch Control XL' <- was the KeyStep So the two I fixed are wrong again and the one I called broken is now right. An address is a lease, not an identity. This module resolves "Launch Control XL" by name on EVERY respawn, so a replug costs one backoff interval instead of a silent session. First run against the live rig, with zero configuration: starting port=None ok port=24:0 It also keeps the last value of every CC, and — the part that matters — records `seen: false` distinctly from `value: 0`. An untouched control is what silences a Tidal stream (a bare "^NN" with no value yields NO EVENTS), so a cockpit that rendered untouched-as-zero would hide the exact failure it exists to show. And it does not litter. Three smoke runs of this file each orphaned an aseqdump to pid 1 — precisely the mess PLN complained about this morning ("i see 4 aseqdumps... you had one job"). try/finally is not enough, because SIGKILL, OOM and `timeout` all skip it. PR_SET_PDEATHSIG puts the guarantee in the kernel: the tap dies with its parent however the parent dies. tools/master-out.py — Ardour Master follows a preference list ------------------------------------------------------------- PLN: "can we control this tedious routing... ideally theres a tie breaking list of choice, from uhc to headphones to speakers". Ardour's master does not follow the PipeWire default sink, so every rig move means re-patching in qjackctl. Not "route to X" but "route to the best X actually present": UMC -> Headphones -> Speakers, matched as substrings of the node name so serial numbers and profile suffixes do not matter. Two properties make it safe to run mid-set: * IDEMPOTENT — computes wanted links, diffs against existing, applies only the delta. Already correct = zero pw-link calls = cannot glitch the audio. The naive unlink-then-relink is a guaranteed dropout even when nothing changed. * EXCLUSIVE BUT NARROW — removes Master links to other sinks (or plugging the UMC back in gives you doubled output), and touches nothing else. The limit is a predicate, `_is_ours`, not a good intention: apply() refuses to unlink any source that is not a Master output. Verified against KNOWN-BAD states, not just against agreement with reality — the whole lesson of this session. Ten checks: preference order at each of the three tiers, the "master stranded on headphones when the UMC appears" case (2 adds + 2 removes), idempotency (0 and 0), and the guard refusing both a foreign link in plan() and a foreign source handed straight to apply(). All pass. Live dry-run reports "already correct — 0 changes".PLN (Algolia) authored -
$ midiOn "^76" ((# delay 0.6) . (# delayt "e") . (# delayfb 0))) ^ The `^59` block twenty lines down uses the same idiom correctly with a single outer paren, so this is a typo, not a misunderstanding. Worth recording how it was found, because it was not found by reading. Running tools/silent-eval.py over the track returned: rose_rouge: THE TRACK DOES NOT COMPILE — it will fail live too. Every dN in its block is dead. rose_rouge.tidal:21:68: error: parse error on input ')' One parse error silences the WHOLE do-block (reference_tidal_silent_failures) — not just d4. Twelve orbits, no sound, no obvious cause at the desk, and the only signal is a GHC error in a console nobody is looking at mid-set. With the paren removed, the same query is honest about what is left: FAIL — 2 orbit(s) SILENT SILENT d4 (line 19) (nothing in 64 cycles) SILENT d11 (line 71) (nothing in 64 cycles) Both for the reason predicted before running it: d4 carries `# crushbus 41 (range 16 4 "^35")` and d11 carries `# delay (range 0 0.9 "^19")` — bare "^NN" inside a `#`, which yields NO EVENTS on an empty control map. And `silent-eval --seeded` returns `ok` for every orbit, which is the same fact from the other side. That is a cold query against an EMPTY map, so it describes a rig whose boot seed did not run. Whether this one's did is a separate question and not yet answered here — see #156, which I had wrong.PLN (Algolia) authored -
First playable pass at Rose Rouge. 118 BPM, the `rose` bank carrying both the voice and the horns, and the d5 line walking the sample index by hand ("<5 6 7 8 9 10 11 11 12 13 14 13 15 16 17 18 20 21 22 23 ...>") because the phrase order IS the arrangement — LE TOUR DU JAZZ. The d7 vocal chops keep their own lookup table in the comments, mapping `end` to the words it lands on: 0.2 i wan' 0.3 i want you 0.5 i want you to get to? That table is the score. Committing it now so the next session does not have to re-derive it by ear. Committed as an MVP, warts included, because three of its lines are sitting on the unseeded-control bomb found this afternoon (#156) and I want the BEFORE state in history: * d1 midiOn "^41" -> orDef 0, so the 4/4 floor variant plays on 0% of cycles and the syncopated one plays forever * d4 # crushbus 41 (range 16 4 "^35") -> BARE ^35 in a `#`: no events at all, the bass is structurally silent until that knob is moved * d11 # delay (range 0 0.9 "^19") -> same shape, same silence None of that is a bug in the track. It is the boot's control map starting empty, and the fix lands next commit. This file is the test case.PLN (Algolia) authored -
Four silences in one afternoon, with a gig that night, and every health check reporting perfect: SC up with zero restarts, Tidal holding 6010, 48 graph links present, master at -0.2 dB, interface clocking 48 kHz both ways. All true, all useless. What cracked it was PLN mentioning the system beeps had gone 'legato 0.2'. A device that truncates a short sound is a device being restarted, and nothing in the stack could see that because every lens sampled a moment and the fault lived between them. trigger_time turned out to be the only restart counter anything publishes: the UMC's playback stream had restarted 26 times in 15 minutes while its capture stream — which drives the whole graph — never restarted once. The log's real subject is the two tools I built to find it, and the fact that both shipped blind. rig-pulse called SOUND on 'synths > 0 or peak > 1.5%' when this rig idles at 71 synths and 17.7%, so it read healthy straight through a silence its owner was complaining about. Then it learned the floor as a running minimum, which fails because the floor rises. Meanwhile audio-lens compared consecutive polls with a half-second minimum gap while polling every quarter second, so its rate and stall checks never ran at all. A check that never fires is indistinguishable from a check that passes, and the only reason I found all three is that the rig kept obligingly breaking while I watched.
PLN (Algolia) authored -
rig-pulse answers one question when the sound stops: did Tidal stop asking, or did the engine stop playing? It got that wrong twice, in two different ways, and both are the same mistake wearing different clothes. v1 assumed a universal idle threshold: SOUND if synths > 0 or peak CPU > 1.5%. But this rig idles at 71 synths / 3531 ugens / 17.7% peak — start_and_midi.scd registers the Mutable-Instruments global effects on all 14 orbits and those synths never die. So the tool reported SOUND continuously, straight through a silence PLN was actively complaining about. It could not have fired. I shipped a check that cannot fail one commit after writing a message about how a check that never fires is indistinguishable from a check that passes. v2 learned the floor as a running minimum. Also wrong, and only live data showed it: the floor RISES as more of the graph gets instantiated — 71 synths at 15:02, 103 at 15:05. A baseline that only ratchets down cannot track a rig that ratchets up, so everything above the all-time minimum looks like activity forever. What separates the states is stillness, and the data said so plainly: an idle SuperDirt returned ugens=3531 five times over eight seconds — exactly, not approximately — while a played rig moved 4144 -> 4300 in two seconds. So compare the last few samples to each other and ignore the level entirely. That is immune to the floor changing between boots or during one. Known limit, in the header rather than hidden: a perfectly static drone with a constant voice count would read SILENT. Tidal events are discrete and the count moves, so it holds for how this rig is played. Verified against both states: frozen at 103 synths / 4144 ugens -> SILENT; moving 4066-4222 -> SOUND.
PLN (Algolia) authored -
Companion to audio-lens.py: that watches the audio DEVICE, this watches the audio ENGINE. Together they answer, in one line, the question that cost PLN three interruptions today — when the sound stops, did Tidal stop asking, or did the interface stop playing? scsynth already answers /status on the port it is listening on, and the reply carries the live synth count and peak DSP load. SuperDirt rendering silence sits at its idle floor with no synths; SuperDirt fed by Tidal spawns a synth per event and the load moves. So three states, and the middle one is the point: synths > 0 or peak above the floor -> Tidal is emitting answers but flat at the floor -> engine fine, NOBODY IS ASKING no answer -> the server is gone Nothing in this repo could produce that middle verdict before. During today's stop every existing lens reported healthy — sclang's OSC loop answered, scsynth answered, all 48 SuperCollider->Ardour links were up, the interface clocked a steady 48 kHz — and the sound was gone anyway, because Tidal had stopped emitting while its process stayed alive, threads scheduled, port 6010 held. Written with the offsets bug already paid for. The first throwaway version of this query hardcoded byte offsets into /status.reply and reported 1.68 BILLION ugens and 0.0% CPU, and I read the CPU figure off it and told PLN the engine was rendering silence. It was not: reading the typetag properly gives 91 synths at 30.7% peak. A number obviously wrong enough to notice sat right next to a number wrong enough to mislead, and I used the second one. Hence osc_parse walks the typetag, and the absurd-value story is in the docstring so the next reader distrusts hand-rolled offsets on sight. Passive: /status is a query, allocates nothing, never touches the default server object, never plays a tone.
PLN (Algolia) authored -
Three fixes, found by pointing the tool at the laptop's built-in card when PLN moved upstairs and played UMC-less out of the aux jack into a Pioneer amp. 1. The rate and STALL checks never ran. They compared consecutive polls and required dt > 0.5 s, but the default poll interval is 0.25 s, so the condition was almost never true: the 900 s watch that diagnosed the UMC collected exactly TWO rate samples. The restart detector carried the entire investigation while the other half of the tool was decoration. Now measured against a reference sample held at least a second back, which also makes 'RUNNING but hw_ptr frozen' a check that can actually fire. Verified: 11 samples in 12 s, 47984/47996/48034 Hz. 2. It watched pcm0p unconditionally. Fine for a USB interface with one playback PCM; wrong for the SOF card, which has eight (headphones, speakers, mics, HDMI) and where which one is live depends on where the jack is. It would have reported perfect health about the speakers while the sound went out the headphone jack. Now prefers a RUNNING substream, re-resolved every poll, and says so when the live PCM moves. 3. nominal_rate read stream0, which is USB-audio-only, so it returned None on the built-in card — disabling the rate comparison on exactly the device PLN falls back to when the interface dies. Falls back to hw_params. Also stops crying wolf: a stream that just restarted has a hw_ptr counting from zero, so the first delta across the seam is meaningless. Measuring it anyway printed 'RATE OFF - 0 Hz (-100%)' on every reconnect during the replug test — a false alarm fired at the exact moment the operator most needs to trust the tool. The lesson is the one this repo keeps relearning: a check that never fires is indistinguishable from a check that passes, and you only find it by running the thing somewhere new.
PLN (Algolia) authored -
'Je jouais, d'un seul coup plus de son.' Every static check said fine: SuperCollider up with nrestarts=0 and a clean log, Tidal's GHC alive and holding 6010, the PipeWire graph fully linked SuperCollider -> Ardour -> UMC, Ardour's master at -0.2 dB reaching both interface channels, the UMC reporting 48000 Hz momentary on both directions. Nothing to find, if you only look once. The tell came from PLN's ear instead: the system volume beeps had gone 'legato 0.2' — truncated. A device that eats the head off a short sound is a device being stopped and restarted, and no lens in this repo could see that, because each one samples a moment and this fault lives strictly between moments. trigger_time is what exposes it. It changes only when a PCM stream is (re)started, so it is a restart counter that nothing else in the stack publishes: no log line, no error, and PipeWire reports the node 'running' on both sides of the event. Reading it twice showed the UMC's PLAYBACK stream had restarted while the CAPTURE stream had been up for 33 minutes straight — and since pw-top shows the capture node is the graph DRIVER, the graph keeps clocking serenely through an output that has gone away. That asymmetry is the whole bug, and it is invisible to every one-shot check. So the tool watches over time and reports what a snapshot cannot: stream restarts with the lifetime of the stream that died, effective rate from hw_ptr deltas (RUNNING with a frozen hw_ptr is a stall, and a rate off nominal is the 'everything plays fast' flavour of the same complaint), state transitions, and device presence across an unplug. Two deliberate choices. It resolves the card BY NAME on every poll rather than caching a number, because a replug can move the UMC from card1 to card2 and a lens pinned to card1 would report 'gone' forever and miss the entire after-half of the replug test this was built for. And it is strictly passive — opens no PCM, plays nothing, captures nothing — because this rig has already had one investigation where the probe's own capture caused the xruns being blamed on the rig. Verified: --once reads card1 with playback age 198s vs capture 2192s, the exact asymmetry above; a 45 s watch over a quiet stretch correctly reported zero restarts (the fault is intermittent, which is itself the finding); the unknown device path lists the cards present instead of failing bare.
PLN (Algolia) authored -
PLN asked for a restart-SC command in the tray menu. The reason it is worth having: a headless sclang has no post window and no eval-over-OSC endpoint, so a restart is the ONLY way to arm the sample-watcher responder or to pick up a sample pack dropped since boot. Today that meant typing a systemctl line into a terminal, mid-compose, which is exactly the friction the tray exists to remove. The catch is that it sits two pixels under 'Launch Gig' and it is the only entry in this menu that can STOP THE SOUND. So while the rig is up it is not a one-click action: the first click arms it and says what it costs, and a second click — which means opening the menu a second time, deliberately — fires. The arming lapses after ten seconds. That is #116's rule ('the hot path cannot start the sound by accident') applied to the opposite accident, and it is why there is no confirmation DIALOG: a modal stealing focus mid-set is its own hazard (#136). When the unit is DOWN there is nothing to interrupt, so it stays a single click. The label carries the state rather than the intent, because the two 'up's are not the same up: systemd reports active the instant sclang execs, but the banks register alphabetically after that and the rig is not playable until the end. Measured today, active -> responder armed took 27 s, so under 30 s of uptime the entry reads 'booting - Ns of ~30s' instead of claiming the sound is back. A silent rig should never be a mystery. Read on menu-open only, never on the 2 s thermal tick: it shells out to systemctl, and paying that 30x a minute for a label nobody is looking at is the per-tick cost this rig keeps getting bitten by. The restart runs through QProcess so a two-second systemctl call cannot freeze the tray. Verified: module imports clean, sc_state() reads (True, 145s) against the live unit, fmt_dur spans 5s->2h13m, tray restarted and came back with no traceback.PLN (Algolia) authored -
The watcher's not-answering message said 'load the responder once, in SC: "sample_watch.scd".load'. Checked that against the live rig and it is not possible here. sclang runs headless under parvagues-sc.service, so there is no post window to type into; its stdin is /dev/null, so nothing can be piped in; and neither sclang nor the ParVagues boot exposes any endpoint that interprets code over OSC. There is no way in. Which means a rig booted before start_and_midi.scd learned to source the responder will never answer /pv/ping, no matter how long the watcher waits — exactly the case right now, sclang having come up 2026-08-13 14:20 against a boot script that only grew the source line at 2026-08-14 11:07. So the message now names the real remedy and its real price: a restart of parvagues-sc, which stops the sound, and therefore belongs between tracks and never mid-set. A wrong instruction is worse than a blunt one — it sends you hunting for a window that does not exist. rose (46 files) and love_parade (5) are still sitting unloaded and unshadowed, waiting for that restart.
PLN (Algolia) authored -
Two complaints weeks apart that sounded unrelated — "the kick doesn't hit" and "the WAP bass is saturated" — turned out to be the same orbit. d4 is 76-91% of the low-frequency bed floor on every buried track AND the exposed bass in WAP. One stem, two jobs, both done badly at different moments, which is what livecoding does to an orbit and what a set-wide mix decision cannot see. Punch median 8.3 -> 10.3 dB, buried tracks two -> one, both masters landing on 4786.370000 s so the fifteen approved boundaries survived untouched. The shareable part is the near-miss: the first render improved every track, INCLUDING the seven with no correction applied. Good numbers, would have shipped. What stopped it was asking why the untouched tracks moved — the balance was attenuating one side instead of holding constant power, so nine pans were also nine undeclared level cuts being credited to the deliberate work. After the fix the median went 10.4 -> 10.3: the result was real and the explanation had been wrong, which is the failure that survives review because nobody audits a number they like.
PLN (Algolia) authored -
PLN, mid-compose: "can we build a parvagues samples watcher tool. watches every min the samples folder. on new found, loads in current service running sc the sample pack?" Three pieces, and the interesting part is what each one REFUSES to do. sample_watch.scd — an OSC responder (/pv/ping, /pv/loadBank) so a new bank can be registered into the RUNNING rig. Sourced from start_and_midi.scd, and safe to evaluate standalone mid-session since OSCdef replaces its own key. Loading is cheap because lazy mode reads WAV headers only. tools/pvbanks.py — ONE definition of "what banks exist", mirroring the boot's three loaders. A watcher and a checker that disagreed about what a bank is would be the worst bug available here: the checker says clean, the watcher loads something else, and the rig plays a sound nobody chose. tools/sample-watcher.py — the loop, plus a systemd --user unit that is deliberately NOT PartOf the SuperCollider unit, because a supervisor coupled to its supervisee launders its own restart limit. WHAT IT REFUSES Shadowing. `~dirt.loadSoundFiles` cannot append even if you ask it to — SuperDirt.sc:132 passes `appendToExisting = false` as an ASSIGNMENT rather than forwarding the argument, so every load replaces. A pack shipping a folder called `kick` would therefore free the existing bank's buffers and take the name, changing what an old track plays with no error anywhere. Any new name claimed by two folders is reported and SKIPPED for a human. Claiming success at a silent rig. It pings first, and a load that gets no reply is NOT written to the state file, so the next pass retries instead of believing itself. Verified against a mock rig: no-reply pass loads 0 and marks nothing known. TWO BUGS CAUGHT BEFORE THEY SHIPPED PathName.folderName returns the PARENT for a directory path — it is built for file paths. The responder would have looked up the wrong bank and reported a wrong file count on every success. It now diffs soundLibrary.buffers.keys before and after, so whatever key APPEARS is the bank name by construction, and no assumption about basename-with-trailing-separator is needed. My own protocol test wrote to the real state file, which then made --dry-run report all 1425 banks as new. Fixed the test isolation and, separately, made the baseline branch honour --dry-run instead of saving. Also verified against the real tree: 1425 banks, 19396 playable files, zero shadowed names. File counts follow SuperDirt's pathMatch("*"), which skips dotfiles — the AppleDouble `._X.wav` trap that once made a folder look clean to `ls` while a naive glob double-counted it. NOT YET VERIFIED: the .scd against a live SuperDirt. PLN is composing and the responder needs one eval in his session; the Python half is proven end to end against a mock.PLN (Algolia) authored
-