1. 16 Aug, 2026 8 commits
    • feat(cut-lens): v3 measures the crossfade as a span — and self-calibrates · dab8d38b
      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
    • docs(archive): the set-judge, and what it found on first use · 60fbec90
      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
    • feat(cuts): cut-lens v2 — better, still not trustworthy, and now we know why · d683d786
      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
    • feat(judge): sticky header, and Desire is cut from the release · 73e9d798
      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
    • docs(ear): the floor problem, measured — kick audible 59% of OPAL-26 · f1798e7b
      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
    • fix(judge): chain into the next track — and PLN's ear beats both cut detectors · 20b91515
      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
    • feat(judge): a set-judge — 15 tracks, a verdict each, one decisions.json · 9c6a24cb
      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
    • feat(rig): parvagues-protect — keep scsynth un-killable across every restart · 5a97d0ed
      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
  2. 15 Aug, 2026 3 commits
    • feat(tools): button-column-audit — 174 tracks still speak the pre-remap layout · f92f4455
      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
    • fix(sc): swap PV_Freeze for PV_MagFreeze — JoshPVUGens took the server down mid-set · e65e3a87
      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
    • feat(slop): three output shapes and a full-length cut, so YouTube is reachable · 113d9c57
      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
  3. 14 Aug, 2026 17 commits
    • feat(gig-log): record what was PLAYED, not what was looked at · 97b22c0b
      #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
    • fix(lcxl): the tap went deaf for six hours and every signal stayed green · e7961f57
      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
    • fix(metadata): teach gig-metadata both segment shapes, and refuse to guess · f584a1c2
      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
    • docs(log 024): an address is a lease · bee4f240
      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
    • feat(nujazz): rose_rouge grows a d6 counter-line and an Orleans break · 1a5fa811
      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
    • feat(rig): one supervised surface tap, and an output that follows a preference list · 560e0e95
      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
    • fix(nujazz): one paren too many killed all twelve orbits of rose_rouge · 098d5712
        $ 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
    • feat(nujazz): rose_rouge MVP — St Germain's tour du jazz, twelve orbits deep · ca39a9e1
      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
    • docs(log 023): the checks that could not fail · bfb2e2d5
      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
    • fix(obs): detect activity by MOVEMENT, not by level — two wrong versions later · c53937a9
      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
    • feat(obs): ask the synth whether the rig is making sound · 11638458
      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
    • fix(obs): half the lens was dead code, and it took a device swap to notice · 0d184046
      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
    • feat(obs): a lens that can see the fault between the checks · 13c12b62
      '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
    • feat(tray): restart SuperDirt from the tray, but not by accident · b9677d23
      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
    • fix(samples): the watcher told you to do something you cannot do · 82d7a533
      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
    • docs(log 022): one stem did it · 03328ace
      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
    • feat(samples): drop a pack in, keep playing — the rig picks it up in a minute · 446d7a44
      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
  4. 13 Aug, 2026 1 commit
    • docs(log 021): the editor knew all along · c4e70571
      Captain's log for #148. PLN heard the album's cuts were wrong within seconds of
      a private SoundCloud upload; the bug went four layers deep, ending at a
      provenance note that borrowed a true r>=0.99 from an entirely different
      measurement.
      
      Two learnings worth the write-up. First: a verifier that passes the case you
      built it for is not yet a verifier — mine passed WAP and Drums, the two tracks
      he had actually complained about. Second, and his line not mine: the text editor
      knew every boundary exactly and never wrote it down, so we spent an afternoon
      reconstructing from audio what a log line would have given for free.
      PLN (Algolia) authored
  5. 12 Aug, 2026 4 commits
    • docs(log 020): the master that was already clipped · 6c16e78c
      Captain's-log entry for the OPAL-26 mastering session (#146). The documentary
      beat: three separate times today something SOUNDED fine while its metadata lied
      — 11 of 12 shipped stems hard-clipped, every master this tool ever made silently
      192 kHz, and fifteen split files each claiming to be the whole 79-minute set.
      None of it errored. An audio pipeline does not fail by crashing; it fails by
      producing a file that opens.
      
      Counterweighted with the hour I spent building an elegant theory about a
      "lingering arp" around an orbit measuring -72.6 dBFS, which PLN closed in forty
      seconds by listening.
      PLN (Algolia) authored
    • docs(notes): the ghost-arp verdict — the premise was wrong, not the orbit · d95f9b4c
      PLN A/B'd five solo-orbit clips against the master and closed it in under a
      minute: "wait rehearing master i dont hear any issue? ignore the arp variants,
      lets keep and lets confirm what we have."
      
      So the OPAL-26 masters stand as rendered. No stem surgery.
      
      Logging it because the near-miss is the useful part. The machine narrowed five
      candidates and could not call it. d12 fit the declaration theory PERFECTLY —
      declared by do_it_right, absent from sunshine, re-declared by take_5, which
      would have explained "gone once in take 5 drops" exactly — and it even locked to
      do_it_right's 89 BPM cycle. It measured -72.6 dBFS. Inaudible. The tempo lock
      was noise floor, and the elegant theory was fitting a signal nobody could hear.
      
      Also captured, since they are real reactions even though they are not the thing
      he asked about: d6 has a ~2 s blast worth killing some day, d3's percs are "a
      bit dry", d10's riser is fine.
      PLN (Algolia) authored
    • docs(notes): OPAL-26 listening pass — PLN's floor feedback, cross-checked · 526d64cd
      Appends the OPAL Festival 2026 section to the archivist's log: floor reactions
      per track, transitions, and the sound/technical notes, each paired with what the
      measurement said.
      
      The pairing is the point. Where PLN's ear and the meter agree, that is a
      calibration datum:
        "not loud enough, i had to set max volume"  -> -18.2 LUFS, 4.2 dB under target
        "MAFIA bass goes too high for a few secs"   -> +24.5 dB over local median,
                                                      60-250 Hz, at 1:02:10 (he filed
                                                      it at 1:03:10 — one digit off)
      And where they disagree, that is more interesting still: Mafia got a strong
      floor reaction while being the NARROWEST (width 0.05) and most bass-dominant
      (78% in 60-250 Hz) track of the night. Width is not what makes these land.
      
      Near-miss worth recording: I first wrote this file with Write, having been told
      twice by a stale tool cache that it did not exist. It did — 319 lines including
      the entire Take89/Montreuil listening pass. 314 lines were destroyed and
      recovered from HEAD. The corpus CLAUDE.md calls a documentary source survived
      because it was committed.
      PLN (Algolia) authored
    • feat(metadata): generate a gig's tracks.json from the parsers, never by hand · cab3ccd8
      OPAL 2026 had NO metadata anywhere — content/lives/ carried 2024 and 2025
      entries for the same festival and nothing for this one. Writing it by hand is
      how wrong strings get copied between files (split_bandcamp.py's ALBUM already
      did that once), so tools/gig-metadata.py reads every fact from whoever owns it:
      
        backlog.md  via tools/setlist.py --long   -> membership, ORDER, name, bpm, section
        the .tidal  via tools/setlist_samples.py  -> the sample packs actually referenced
        --segments                                -> timecodes measured off the take
      
      The timecodes pay for themselves twice. They came from aligning the gig log to
      the audio by cross-correlation (r>=0.99 over 20 probes across 11 orbits, 0.00 ms
      spread), and they let a visualist cut to the set without re-deriving boundaries
      by ear — which is exactly what Slopmotion needs.
      
      OPAL 2026: 15 tracks, 89-166 BPM, 60 sample packs, 1:20:09 — and the set was
      played in the EXACT planned order, 15 for 15.
      
      Also two pan TODOs, measured rather than felt: mafia_sans_serif is width 0.05
      and ghosts_in_the_toilets 0.09 (side/mid RMS) against a set median of ~0.20 —
      the two narrowest tracks of the night, and they played back to back. Both files
      also carry PLN's own gig-day edits; both still pass silent-eval --seeded.
      PLN (Algolia) authored
  6. 03 Aug, 2026 7 commits
    • chore(cheatsheet): trailing-newline tidy from the print pass · add4e98f
      Whitespace only — PLN opened docs/GIG-CHEATSHEET.md to send it to his phone
      and print it for the rehearsal, and the save stripped the trailing blank line.
      Committing it so the tree is clean before travel: an unexplained dirty file is
      exactly the shape a Pulsar buffer-clobber leaves behind, and the next session
      should not have to re-derive that this one was benign.
      PLN (Algolia) authored
    • docs(evening): Window:Reload does NOT undo a buffer clobber — close the tab · 22ea436d
      Self-correction before it misleads anyone at 2am in a tent. §1 told PLN to use
      `Window: Reload` to pick up the on-disk desire.tidal fix. That is wrong, and it is
      wrong in a way already recorded: Reload restores buffers from Pulsar's SESSION
      CACHE, not from disk. PLN's own words when we learned this: "cause reloads dont do
      it". The only reliable manual action is to close the tab and reopen the file.
      
      Also names tonight as the trap's THIRD recorded bite, and its nastiest shape so far:
      it reverted an on-disk fix within minutes and thereby made a correct fix look wrong
      in a verification loop — the earlier instances were noticed as a wrong knob, this one
      as a wrong test result.
      PLN (Algolia) authored
    • docs(evening): tomorrow is not a morning — the tent brief for J-4 · ddc34912
      PLN: "so tomorrow its not a MORNING.md note its an EVENING.md one :P" — he
      travels at dawn, so the next time he and the rig are both awake it is evening, in
      a tent, on headphones. MORNING.md's whole frame (the KRKs, the room, the last
      speaker day) is spent.
      
      Written for that context specifically: what needs ears vs what needs the room,
      and §1 first because it is the only item that can destroy work — Pulsar is
      probably still holding the broken desire.tidal buffer, and it already clobbered
      this fix once tonight.
      
      Carries forward the two things he has not heard yet (1-cycle xfade with the -6 dB
      cut-group explanation, desire's repaired midiOn bass), the fact that NOTHING from
      tonight was recorded and why that promotes #37 to a blocker, the unchanged ghost
      list with the paste-at-top remedy, and the rig health numbers measured mid-set.
      
      Also records the MIDI fork he diagnosed himself, because it is the kind of thing
      that reads as impossible the second time it happens: Tidal hears the LCXL over
      ALSA seq (24:0 -> SuperCollider in4), Ardour hears it over PipeWire's Midi-Bridge
      into `ardour:MIDI Control In` via Midi Through. Break either and the other stays
      perfect — which is exactly why "midi passes to midiOn/off but no fader moves"
      was a real, coherent symptom and not a contradiction.
      PLN (Algolia) authored
    • fix(desire): the track did not compile, and the error pointed at the wrong block · 70048d46
      Caught by tools/silent-eval.py on the four tracks PLN saved at the end of the
      last speaker rehearsal. `desire` did NOT compile — meaning the bass he had just
      called "doope" existed only in a running ghci, in a take that was never recorded.
      A saved file that does not compile is the worst kind of gig blocker: it looks
      like work is safe.
      
      THE ERROR LIED ABOUT ITS LOCATION
      
          <track>:409:7: Couldn't match expected type: Pattern ValueMap
                                      with actual type: Pattern ValueMap -> Pattern ValueMap
            Probable cause: 'midiOn' is applied to too few arguments
            In the first argument of '(#)', namely 'midiOn "^45" (# crush 6)'
      
      Line 409 of the generated file sits in the "Claude's Gesture" block, so that is
      where I looked first, and a plausible fix there changed nothing. `--keep` and a
      read of the actual generated Haskell found it three blocks earlier:
      
          orb4_51 = idcp $ gF2 $ gM3
              $ midiOn "^45" (# crush 6)      -- a FUNCTION
              # "bassWarsaw" # voice 0.05     -- used as the LEFT operand of #
      
      "Claude's Weight" had exactly one note figure, "0@5 0 <2 3> <1 0>", and that
      figure moved into Gesture's new midiOff. What remained was a midiOn with no
      pattern to apply itself to. GHC unified across the whole d4 group and reported
      the position where the type conflict surfaced, not the block that caused it.
      
      TWO FIXES
      
      1. "Claude's Weight" commented out, params and all, with the reason inline.
         Nothing musical is lost — the LAST d4 in a file wins, and that was never this
         block. Restoring it needs a note pattern of its own, which is a taste call.
      2. "Claude's Gesture" — `midiOn ch` passes its argument straight to
         someCyclesBy (BootTidal:391), so it must be a FUNCTION. Both note figures are
         now LEFT sections of `#`:  ((note (scale ...)) #). That keeps the original
         semantics exactly: note supplies the rhythm, bassWarsaw and the params merge
         in. ON = the moving arp, OFF = the grounded root, which was the intent.
      
      VALIDATION: `silent-eval.py --seeded live/collab/raph/desire.tidal` -> "ok —
      every declared orbit emits events". Cold, no rig, no second ghci.
      
      PROCESS NOTE WORTH KEEPING: the first attempt at fix (2) was written to disk and
      then silently reverted — PLN quit Pulsar and reopened it, and Pulsar wrote its
      stale BUFFER back over the file. That is #95 / the buffer-clobber trap, observed
      live rather than theorised. Any on-disk fix to a file open in Pulsar needs a
      Window: Reload before the editor touches it again.
      PLN (Algolia) authored
    • chore(set): PLN's Monday keepers — desire finally gets its bass · 491b04a7
      The last speaker rehearsal before OPAL. Saved as played, not as I would have
      written it: these are ear decisions and the ear was in the room.
      
      desire.tidal — the "FIXME BASS!!!" is answered
        * 127 -> 128 BPM.
        * d4 Claude's-Gesture bass is now a TWO-STATE gesture instead of one line:
          `midiOn "^44"` takes the busy arpeggiated figure
          ("<[0 3 0@14] [0 <2 4> 0@14] [3 0 2@14] [0 3 <0 -3>@13 ~]>") and
          `midiOff "^44"` holds the sparse root ("0@5 0 <2 3> <1 0>"). One button now
          moves the bass between grounded and moving, which is what the track wanted.
        * d3 stops fighting itself: the 32-bar gain swell (range 1.3 1.6 saw * per-hit
          velocity) is parked as a flat `# gain 0.9` with the swell kept commented
          directly beneath it, and `legato` goes back to being a knob.
        * d5 named: "Piano Desirable show".
      
      mafia_sans_serif.tidal — d7, the one that was "ACID WEIRD NOT GOOD": `slow 2`
        becomes `fast 2` and gain 1.1 -> 1.5. It sits up now.
      
      you_my_sunshine / vague_de_crime — small level and structure trims from the
        Sunshine -> Vague seam work.
      
      backlog.md — BPM annotations on the tail: Desire [129], le shifteur marteau
        [130], REVOLUTION [114]. backlog.md is the setlist SSOT (#123), so these are
        the numbers the tooling will read.
      
      Recorded verbatim, INCLUDING two things I have not touched and am reporting
      rather than silently fixing:
        * desire d3 now reads `# legato (range 0.05 0.9 "^18")` while the comment above
          it still says "^52: tightness". If ^18 belongs to another orbit's column that
          is the #54 borrowing problem live on stage — flagged, not edited.
        * NOTHING FROM TONIGHT WAS RECORDED. "Tidal Live" holds 120 wavs, newest dated
          2026-08-02 (Take95); zero files today, no open write fds. PLN: "forgot to
          record!" The basses above exist only as code. That is #37's whole reason for
          being, and it is now getting a red warning in the HUD.
      PLN (Algolia) authored
    • fix(xfade): 4 cycles was too slow to work with — and the dip was real · 15695de4
      PLN, mid-rehearsal on the last speaker session before OPAL: "atm when i work
      the track and change eg. a # gain 1.4 to # gain 1.5, it goes whole track all
      d1-d12 through fadedown-fadeup over a few bars?? its not ok, i cant adjust
      anymore." Then the good question: "fade d1 X -> d1 X should be constant, as
      each percent of volume removed from X is added to id?"
      
      He is right about the arithmetic and the rig breaks it in three places.
      
      WHY CROSSFADING A PATTERN WITH ITSELF DIPS
      
        1. envEq/envEqR are EQUAL-POWER envelopes, g(t) = sqrt(sin(pi/2*t)), so
           g_in^2 + g_out^2 = 1. Correct — for a LINEAR gain stage.
        2. SuperDirt's law is amp = gain^4. At the midpoint both sides sit at
           g = sqrt(0.7071) = 0.8409, so each half's amplitude is 0.8409^4 = 0.5
           EXACTLY. The equal-power promise was made in the wrong domain.
        3. `overlay` fires the same sample TWICE. Two coherent voices would sum
           0.5 + 0.5 = 1.0 and be flat — that is PLN's model. What actually happens:
      
             orbit with `# cut N`   2nd voice releases the 1st (playSynths broadcasts
                                    the cut group BEFORE amplitude is read — the same
                                    mechanism as the 2026-07-29 ghost bug), one voice
                                    survives at amp 0.5              => -6 dB
             orbit without cut      two independent voices, incoherent sum,
                                    sqrt(.5^2 + .5^2) = 0.707        => -3 dB + comb
      
      Deepest at t/2, unity again at t. With t=4 that is a four-bar swell on twelve
      orbits at once — because each dN is its own transition and ctrl-enter re-sends
      the whole blank-line-delimited block, all starting at the same `now`. Editing
      one `# gain` changes one line but evaluates twelve.
      
      THE STANDING CONCLUSION: an overlap crossfade can NEVER be transparent on a
      cut-group orbit. `cut` means "one voice in this group"; a crossfade means "two".
      Shortening the fade does not remove the dip, it makes it a blip instead of a
      swell. The dip-free answer is a NON-overlapping transition (`clutchIn`), which
      changes the feel of every seam in the set — so it waits for after OPAL (#13).
      This file's own comment guessed that in July; it is now measured, not guessed.
      
      CHANGES
      
      - xfade default 4 -> 1 cycle. One token, so it is one token to flip back.
      - xfadeCutIn gains a `t <= 0` branch returning the incoming pattern untouched.
        Needed before "0 = instant" can ever be offered: Tidal guards
        `_slow 0 _ = silence`, and `q |* gain silence` then has no right-hand values
        to match, so the INCOMING pattern comes out EMPTY. Unguarded, "0 cycles"
        means silence, not instant. Now `xfadeIn N 0 $ ...` is a real instant swap.
      - Killed a trap in this file's own guidance: it recommended `p N $ ...` for fast
        iteration. `p` carries no `|< orbit`, so the sound lands on orbit 0 — wrong
        LCXL column, wrong Ardour stem, fader that looks dead — the exact failure the
        lines above it warn about. Points at `jumpTo N` / `xfadeIn N 0` instead.
      
      VALIDATION: tools/check-boot.sh (safe mid-set, never grabs 6010) — helper block
      typechecks against tidal-1.9.5, all 13 g* helpers audible with an untouched
      controller, and the transition ghost check still reports 0 outgoing onsets after
      the fade. Takes effect at the next Tidal boot; nothing in the running session
      changed.
      PLN (Algolia) authored
    • docs(armada): the J-5 dropout, logged as the documentary it is · 5367121e
      Every intuition on the way to this one was wrong, including several of mine, so the
      log records the wrong turns as well as the answer — that is the part worth reading
      back.
      
      The shape: nothing crashed. sclang up, scsynth up on 6.7% CPU, sink RUNNING,
      routing intact, and no sound. Any monitor aimed at SuperCollider would have
      reported a healthy rig. PLN found the discriminator himself by switching the
      default sink by hand — internal speaker plays, UMC does not — which killed the
      pure-CPU theory and pointed at the interface, which was also not at fault: 128
      vs 1024 period_size is an 8x tighter deadline, so the UMC is the canary.
      
      Recorded because they will recur:
        - `state: RUNNING` lied while the device was dying once a second; only repeated
          hw_ptr sampling is honest, and FOUR samples, not two — my two-sample read
          called it "still stalling" when a ring-buffer wrap looks identical to a stall.
        - a rule scoped to one device but applied to the whole graph stops being a fix
          and becomes a load (52-no-suspend matched every node; two nodes nobody used
          held 2.1M xruns).
        - the laptop mic had auto-wired itself into the d1 stem, invisible in Ardour's
          GUI and in the saved session, and Take95 is already exported (#132).
        - #7 survived two fixes because the metric counted the BOUNDED map.
        - my own journalctl scans burned a core twice while he was losing audio. I was
          the load I was measuring.
      PLN (Algolia) authored