1. 15 Aug, 2026 1 commit
    • 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
  2. 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
  3. 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
  4. 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
  5. 03 Aug, 2026 12 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
    • chore(perfect): PLN's rehearsal keepers — BURN and DEEP, and d5 gets its filter back · 8cd149b2
      Uncommitted after the Monday session. His, tuned by ear, and sitting only in the
      working tree — the state that let a stale Pulsar buffer eat committed work twice.
      
        d3  gains `# att (range 0.2 1 "^31")`.
        d4  the two bus knobs SWAP: crushbus moves to ^52 and octersubbus to ^32, each
            now labelled in his own words ("BURN", "DEEP"). Both are legal d4 slots, so
            this is a taste decision about which hand reaches which effect, not a
            correction.
        d4  the two FIXME(#54) lines and the commented-out `octersubbus 43 ^17` are
            GONE. That was the third-effect overflow parked pending gSel; he resolved it
            by dropping the third effect instead of waiting for the fold.
        d5  `$ gM3` becomes `$ gM3 $ gF3` — the DJ filter is back on the SuperStars
            layer, replacing the commented `-- $ gF3 $ gMute3` above it.
      
      Note the d4 swap is exactly the pair of slots that a buffer clobber has reverted
      before (both effects landing on ^52). They are on ^52 and ^32 here — distinct, so
      this is the healthy state, not the clobbered one.
      PLN (Algolia) authored
    • fix(audio): prune the graph — 2.1M xruns came from nodes nobody used · db42ffce
      THE J-5 DROPOUT. Sound went glitchy-then-silent mid-rehearsal, five days before
      OPAL, and every obvious suspect was healthy: sclang up, scsynth up and using
      6.7% CPU, the UMC sink present and RUNNING, every link in place. Diagnosing the
      audio engine would have found nothing wrong with it, because nothing was.
      
      THE BEHRINGER IS THE CANARY, NOT THE FAULT. The internal speaker played fine the
      whole time and only the UMC202HD died, which looks exactly like a broken
      interface. It is not: period_size 128 on the UMC against 1024 on the SOF speaker
      is an 8x tighter deadline, so the UMC is the only device in the box with a
      real-time deadline tight enough to miss. USB devnum stayed at 14 across samples
      (not the 2026-07-26 re-enumeration bug), mixer 127/127, links intact. Rule for
      next time: when only the UMC is silent, suspect the SCHEDULER.
      
      WHAT pw-top FOUND. Xruns per node:
      
          sof_sdw.HiFi__Headset__source   1,421,369   nothing used it
          sof_sdw.HiFi__HDMI1__sink         704,081   2.8ms BUSY/cycle, no client
          UMC202HD Line sink                 32,338   the victim
          alsa_input.hw_U192k_0              14,338   W/Q 1.24 = waits > a full quantum
          SuperCollider                       1,119
          ardour                                422
      
      Two nodes NOBODY USES produced 2.1 million of them. HDMI1 appeared when the
      screen was mirrored for the WYSIWYG show.
      
      THE ROOT CAUSE WAS A FIX. 52-no-suspend.conf matched ~alsa_output.* and
      ~alsa_input.* — every node in the graph. It was written for a real reason (a
      woken device eats the first note's attack) but applied graph-wide it held six SOF
      sinks and a broken mic source permanently open INSIDE the real-time graph.
      Never-suspend is precisely what it asked for. The generalisable lesson, and it is
      the one worth keeping: a rule scoped to one device but applied to the whole graph
      stops being a fix and becomes a load. Suspension is not the enemy — suspension of
      the device you are playing through is. Now narrowed to the UMC plus the internal
      Speaker, the latter kept deliberately because it is the fallback that proved the
      machine could still make sound during the incident.
      
      AND THE LAPTOP MICROPHONE WAS IN THE d1 RECORDING. pw-link showed
      Headset__source:capture_FL -> ardour:Tidal 01/audio_in 1. Ardour auto-connects
      physical inputs, so the internal mic was summed into the input of the track that
      records d1, alongside SuperCollider:out_1 — invisible in Ardour's GUI and in the
      saved session file. 53-rig-graph.conf disables the source outright rather than
      trusting nobody to re-arm that track. Take95 was recorded with it connected;
      auditing that is #132.
      
      RESULT, measured. Load 7.17 -> 4.80. UMC hw_ptr went from restarting once a
      second — never past ~3000 frames, deltas going NEGATIVE — to a steady 48,145
      frames/sec across six consecutive samples with zero restarts.
      
      VERSIONED, NOT COPIED. The three wireplumber files now live in
      config/wireplumber/ and are SYMLINKED from ~/.config. One source of truth: a copy
      would have become the fourth thing in this project to drift from its generated
      twin. check-audio-graph.sh also fails on a dangling symlink, because a broken one
      is silent until the next wireplumber restart — the worst possible moment to find
      out.
      
      NEW GATE: tools/check-audio-graph.sh, wired into gig-up as a HARD check. It
      asserts what was actually wrong, in the order it bites: no non-SuperCollider
      source on any Tidal input (recording integrity first — a dropout you hear and
      redo, a stem with room noise in it you ship), Master really reaching the UMC,
      the interface actually clocking, and the prune still in effect. check-mix reads
      the SAVED session; this reads the LIVE graph, and the saved file has been wrong
      before (#62).
      
      TWO METHODOLOGY RULES BAKED INTO IT, both learned that day:
      
        - NEVER SCAN THE JOURNAL. `journalctl --since` on a long-uptime box reads the
          whole journal and burned a full core twice WHILE PLN WAS LOSING AUDIO — the
          diagnostic making the fault worse. Everything is /proc, pw-link, bounded
          pw-top.
        - `state: RUNNING` PROVES NOTHING. It read RUNNING while the device was dying.
          Only the hardware pointer, sampled repeatedly, is honest — and take four
          samples, not two: my first two-sample read gave a confidently wrong "still
          stalling" verdict because a ring-buffer wrap looks identical to a stall.
      
      Mutation-checked: the recording-integrity awk was run against the real 2026-08-03
      pw-link dump WITH the mic present (it fires) and against a clean one (silent, no
      false positive). A check that has never seen the bug it was written for is a
      guess.
      PLN (Algolia) authored
    • feat(tray): Launch Gig — and Wayland can focus a window after all · 05df7fee
      PLN: "wheres the 'run parvagues' big HUD menu button? I see perf modes, open
      bridge, then the gear 4/6 up, but below open bridge, i want a 'Launch Gig' button
      that opens and focuses auto the parvagues".
      
      Two problems behind that one sentence.
      
      DEPTH. Pulsar was reachable only through Gear ▸ Pulsar — the hot path buried
      under the tool drawer. It is now a top-level entry directly below Open Bridge,
      labelled with what the click will actually do ("focus ParVagues" when it is up,
      "open ParVagues" when it is not), because "Focus" vs "Open" is the difference
      between a confident click and one that looks like a no-op.
      
      FOCUS DID NOT EXIST. launchers.py's docstring said "on Wayland there's no
      portable window-focus", so clicking a running app answered with the string
      "already-running" and did nothing. That is true of the X11 toolbox — xdotool,
      wmctrl and kdotool are all absent here and the first two cannot work under
      Wayland anyway — but it is not true of THIS desktop. Plasma exposes KWin's
      scripting engine on the session bus, and a script running inside the compositor
      is simply allowed to activate a window.
      
      So focus_window() registers a tiny KWin script, runs it, and unregisters it. It
      matches on resourceClass (the app id), not the window title: the title changes
      with every file PLN opens, and matching it would break the moment a filename
      stopped containing "Tidal". The script name carries our pid so two clicks cannot
      collide on an already-registered name. Verified live against the running Pulsar:
      focus_window('pulsar') -> True.
      
      Consequence worth noting: running native apps in the Gear menu used to be greyed
      out, because the only honest thing a click could do was refuse. They are live
      again — clicking a running app now raises it, which is the thing you actually
      want mid-set.
      
      Deliberately editor-only. Launch Gig does not start SuperCollider, boot Tidal or
      arm Ardour: #116 is the standing rule that the hot path must not be able to start
      the sound by accident, and one wrong click before a set is a stuck scsynth rather
      than a convenience.
      
      Every failure path falls back to reporting. A focus button that quietly does
      nothing is a papercut; one that throws during a gig is not acceptable.
      
      Bridge suite: 43 passed.
      PLN (Algolia) authored
    • docs(last-third): the closing three, read cold before the last monitored run · 21a55777
      The KRKs go home after tomorrow's dawn rehearsal, so this is the last run with
      real monitors — and desire / REVOLUTION / electric_hammer have not been played
      this cycle at all. Read them cold so his ears go to the music.
      
      Two findings worth the read, both the same shape as Sunday's do_it_right bug:
      
      desire declares d4 THREE times in one block (lines 50, 56, 62), under his own
      "-- FIXME BASS!!!". Later wins, so the two takes he tuned are dead code. The
      winner differs in ways that do not read as a choice: it has no lpf on ^32, so
      that knob is dead on the bass, and it carries no gain at all where the other two
      say 1.15 — which under SuperDirt's quartic law (amp = gain^4) is 4.8 dB quieter
      than the level he dialled.
      
      electric_hammer declares d5 twice (32, 46). The superfreak voice wins, so ^89
      and ^33 have nothing behind them, ^57 does something other than what the dead
      copy above it describes, and hammer:7 — the sample the closing track is named
      after — never sounds.
      
      Also: the five closing seams with their ghost orbits, paste-ready orphan guards
      for the two worst, his two vague_de_crime TODOs with the gain converted to dB,
      and the gate result (GO, 12/12).
      
      One methodology note recorded in the doc: silent-eval must be run --seeded here.
      Unseeded it reports 12 SILENT orbits across these three tracks, every one of them
      a midiOn gate sitting at default-off. That is the harness, not the music.
      PLN (Algolia) authored
    • chore(set): PLN's Sunday-night keepers — "sounds are great now" · f9ec2046
      Four tracks carried uncommitted edits after the KRK run. They are his, they
      are tuned by ear, and they were sitting only in the working tree with a
      rehearsal scheduled for dawn — exactly the state that let a stale Pulsar
      buffer eat committed work twice before (#95). Committing them so tomorrow's
      run starts from disk, not from whatever a buffer happens to remember.
      
      What he changed, and why it reads as deliberate:
      
      - gimme_acid  d5 gains a `^56` jungle_breaks:15 swap and the slice pattern
        moves under `whenmod 16 8`, so the chop only bites for half of every 16
        cycles instead of running flat the whole time. d11 gets `# gain 1.5` and a
        renamed comment ("BOOM, BOOM, BOOM").
      - perfect     comment-only: d1 "Famous Clubkick", d2 "Clap Mechamment
        Celebre". Naming the kick and the clap is how he finds them mid-set.
      - bombe_dj    `resetCycles` commented out. This is the SET OPENER after
        quand_on_decolle; resetting the clock at the top of track 2 yanks every
        still-ringing orbit's phase from the track before it.
      - vague_de_crime  d1 kick +0.3 gain, d6 gains `midiOn "^90" (ply 2)`, and two
        TODOs written in his own words: the kick wants comparing against the rest
        of the set, and d3 is "less solid than in do it right or drops or ouais".
      
      The two TODOs are ear-work for the last-third rehearsal, not defects to fix
      blind — logged rather than acted on.
      PLN (Algolia) authored
  6. 02 Aug, 2026 5 commits
    • feat(take95): the album exports, and the split was 11 seconds out · d36b478b
      Master: 61.0 min, 2 ch, 48 kHz, 24-bit. I = -14.0 LUFS on the nose, peak
      -0.9 dBFS under a -1.0 ceiling, LRA 12.7 LU. L-R difference RMS is -22.4 dB, so
      it is genuinely stereo and not dual-mono — worth measuring rather than assuming,
      given the first attempt collapsed to one channel.
      
      THE BUG THAT WOULD HAVE SHIPPED. take-master trims leading silence before
      loudnorm sees it, so the master starts 10.94 s later in the music than the raw
      take does. The segments were computed against the RAW take. Splitting with
      unshifted offsets puts every boundary eleven seconds late — each track beginning
      eleven seconds into the previous one's tail. The output would have played
      perfectly and been wrong throughout.
      
      Two guards in take-segments:
        --shift   subtract the measured head trim. Clamped at 0, because the first
                  track legitimately goes negative: PLN focuses a .tidal a few seconds
                  BEFORE he evaluates it (log says 7.4 s, first sound is at 10.94 s).
        --master  probe the mastered file and clamp to its duration. The master is
                  trimmed at the TAIL too (1.45 s), so without this the last segment
                  ran past EOF and ffmpeg would have written a short final track in
                  silence.
      Now: contiguous, starts at 0, sums to 3661.2 s — exactly the master's length.
      
      AND A SHADOWING BUG FOUND WHILE FIXING IT. The per-segment loop bound `start`
      and `end`, the same names as the take window, so the summary line printed the
      LAST SEGMENT as if it were the whole take: "1970-01-01 01:55 — 5.1 min". The
      segments were correct the whole time; only the report lied. That is the kind of
      defect that ships, because the payload looks right and nobody re-reads a header.
      
      GENERATIVE METADATA, per PLN's ask, derived not typed:
        title    backlog.md via setlist.py (the SSOT reaches the ID3 tag)
        TBPM     backlog BPM, NOT setcps — reference_tempo_from_audio, setcps is not
                 felt tempo. gimme_acid is setcps 80 played as 160 half-time DnB, and
                 you_my_sunshine is setcps 144 against a felt 166. Tagging 80 would
                 have been a measured number that misleads.
        grouping the set section (Ouverture / SUNSET / NuJazz / NUIT)
        comment  orbit inventory from the .tidal via pvlint's parser, plus the source
                 path, so any track can be traced back to the code that made it
      
      Metadata says REHEARSAL, not gig: album "OPAL 2026 — rehearsal, J-6", date
      2026-08-02, no venue, no lineup, no poster (feedback_metadata_vs_mastering — gig
      metadata is canonical only in Web/www/next/content/lives/).
      
      lime_tacos is track 7 at 1:49. He played it, then cut it from the set an hour
      later. The take records what happened, so it stays, tagged "not in the final
      set".
      
      Committed: segments.json, track_metadata.json, analysis.json — the provenance.
      Gitignored: the FLACs and MP3s, ~1.3 GB of derived audio regenerable from the
      Ardour stems in two commands.
      PLN (Algolia) authored
    • feat(take-master): the first master came out MONO at 192 kHz — both were real · 844f3dbd
      `tidal-ears master mix` has the right recipe and this reuses it verbatim. What
      it does not know is how Ardour lays out a take, and that produced two defects
      that only appear on this material — both of which I only caught by inspecting
      the output file rather than trusting "Done:".
      
        1. MONO. Ardour writes each orbit as TWO mono files, `Take95_Tidal 04-1%L.wav`
           and `%R.wav`. Globbing all 24 into amix sums them into ONE channel. The
           stereo image is gone — and ParVagues pans deliberately (`# pan 0.8` on the
           hats, `0.2` on take_5's sleepwalker), so that is not a technicality, it
           discards a compositional layer silently. Each pair is now `join`ed into
           stereo first and the mix is 12 stereo streams.
      
        2. 192 kHz. loudnorm resamples internally to 192 kHz to measure true peak, and
           with no explicit output rate ffmpeg keeps it. A 48 kHz session produced a
           624 MB 192 kHz file carrying no extra information. `-ar` is now pinned to
           the source rate. NB this one affects every master the sister tool has made.
      
      UNIFORM TRIM, not per-stem normalisation. gain_for_stem normalises each stem
      toward a target peak, which is right for stems of unknown provenance and wrong
      for these: PLN balanced these twelve orbits by hand, on faders, while playing.
      Normalising individually rewrites that balance — measured at 4.0 dB of shift on
      Take95, mostly making the hats hotter. One trim for every stem preserves the mix
      he played; loudnorm sets the absolute level. Same conclusion take-lens reached.
      
      AND THE MEASUREMENT ITSELF HAD A TRAP. The first version used `volumedetect`,
      which reports max_volume against a fixed-point full scale and SATURATES at 0.0
      dB. Every one of these float stems read as exactly +0.0, so the derived trim was
      -6 dB when the truth needed -19.3. Twelve identical +0.0 readings is not data,
      it is a clipped instrument — `astats Peak_level` reports the real values and
      they match the sister tool's analyze pass exactly (d4 +13.3, d3 -3.3).
      
      Refuses to render if an orbit has only one side, rather than panning it hard.
      Verifies channels and rate of its own output and exits non-zero if either is
      wrong — the check that would have caught both original defects.
      PLN (Algolia) authored
    • chore(take95): the recovered tracklist for the 2026-08-02 rehearsal take · 2bcb605f
      output/Take95/segments.json — 11 tracks over 61.2 min, derived from gig-log by
      tools/take-segments.py rather than detected in the audio. Committed because it
      is the provenance for the album's track boundaries and titles: without it, the
      split is an unexplainable list of timestamps.
      
      Titles come from backlog.md via setlist.py, so they are the same strings PLN
      wrote, not a second copy that can drift.
      
        1. bombe_dj   3:53   |  2. wap        3:19  |  3. do_it_right  8:25
        4. take_5    10:19   |  5. piment     4:30  |  6. ouais_je_funk 4:54
        7. lime_tacos 1:49   |  8. perfect    6:52  |  9. gimme_acid    5:19
       10. vague      6:47   | 11. sunshine   5:04
      
      lime_tacos is in the take at 1:49 — he played it, then cut it from the set an
      hour later ('actually weak'). The take is a record of what happened, not of what
      the set became, so it stays.
      PLN (Algolia) authored
    • feat(faders): stop depending on PLN remembering to raise them · 080a203c
      Third session running, he raised faders by hand and hit Ctrl+S, then asked the
      right question:
      
          "ok raised and saved 10 and 12, saved, check again.
           but ideally we should not depend in that?"
      
      He is right. The gate CAUGHT the problem all three times — the dependency was
      never on the check, it was on him having hands free to fix it. On a Saturday at
      19:00 in a field that is not a dependency you want.
      
      THE MECHANISM, now understood: the Ardour session IS the boot state. He ends a
      set with the faders pulled down, which is what you do at the end of a set,
      Ardour saves that, and the next launch comes up silent. Today's save had NINE of
      twelve at -inf, and all 15 setlist tracks would have lost an orbit — d10 carries
      the riser convention (#33), so all ten risers would have been silent and he
      would never have heard them go.
      
      tools/fader-baseline.py: --capture / --check / --restore.
      
      WHY THIS IS ALLOWED TO WRITE FADERS when the standing rule says never to. The
      rule exists for three reasons — it desyncs the file from the physical desk,
      CC77 down is total silence, and levels are MUSICAL INTENT an agent must not
      invent. All three are about INVENTING a level on a LIVE desk. This does neither:
      it replays a value PLN set and approved (stamped with date and source session),
      it refuses to write under a live session so the desk always wins, and it can
      only address routes named "Tidal NN" — Master, monitor and busses are not
      reachable by this tool at all. It writes a .bak first, because a corrupted
      performance session six days out is not a recoverable mistake.
      
      --capture REFUSES if any fader is silent. Capturing end-of-set silence as the
      baseline would enshrine the exact bug this undoes.
      
      Baseline captured while the mix was green: 12 faders, 01 at 0.0 dB through 09/12
      at +6.0 dB.
      
      PROVEN BY MUTATION on a COPY, never his live session: pull the same nine faders
      to 0.0, --check reports all nine with their baseline deltas and exits 1,
      --restore puts them back, and check-mix.py independently agrees every orbit
      reaches master again.
      
      AND THE TEST FOUND A REAL BUG IN THE GUARD. The first version asked "is ANY
      Ardour running" — the safe-LOOKING answer and the wrong one, because it refuses
      to repair a session nobody has open, and it made the restore path untestable
      while his live session was up. The hazard is writing UNDER a live session, so
      the guard now matches on the resolved session path. Still refuses his open one;
      no longer refuses everything.
      
      Wired in two places: a HARD "fader baseline" check in the gate, and a --converge
      action that restores automatically when Ardour is closed. gig-up: GO, and
      --converge is a clean no-op on a healthy rig.
      PLN (Algolia) authored
    • docs(morning): Monday brief — the set is done, the FADERS are the blocker · ad91ff1a
      Last KRK day. The chart PLN asked for, and the honest reading of it: the SET is
      finished and derived rather than maintained, the TOOLING is done and should stop
      there, and the long pole is REHEARSAL — 11 of 14 tracks played, three never
      heard this cycle, and those three are the FINALE and ENCORE.
      
      gig-up is NO-GO on exactly one thing and it is not code: nine of twelve Ardour
      faders sit at -inf in the saved session (#124, third recurrence). Mechanism now
      suspected — he ends a set with faders down and that is what Ardour saves, so the
      saved session is a snapshot of end-of-set silence. Thirty seconds to fix today,
      but #20's template is the real answer before Saturday.
      
      Pacing measured for the first time rather than guessed: Take95 ran 61.2 min for
      11 tracks = 5:34 each, so 14 tracks is ~78 min against a 90 min slot. Twelve
      minutes of slack.
      
      Also carries the buffer-clobber warning for tomorrow's editing session: close
      and reopen any tab open yesterday, because Window: Reload does not do it.
      PLN (Algolia) authored