1. 28 Jul, 2026 9 commits
    • feat(lcxl): a standalone LED painter — the surface stops being dark, and stops being factory yellow · 60de8a2d
      The problem, in PLN's words, asked three times: "why no button lights, still see
      only A1 green? recover that asap i see no feedback anymore not even yellow
      lifeline" — then "back from 'all yellow' to 'our coding, but touch-reactive and
      persistent post touches'".
      
      Three failures were stacked:
      
      1. NOTHING PAINTED AT BOOT. The only LED painter lived in the Pulsar HUD
         (lib/lcxl-leds.js), so the board was lit only if Pulsar was open, had activated
         the package, and its frame pipeline worked — and #57 says its lastFrame dedupe
         latches an early empty frame, so even with Pulsar open one bad first frame means
         a permanently dark surface with no error anywhere.
      2. FACTORY YELLOW WINNING — a real Midi-Through feedback loop echoing velocity 127
         back as an LED colour byte, not a device default.
      3. NO PERSISTENCE. The LEDs are write-only, there is NO readback, so software must
         own the state and re-assert it. Nothing did.
      
      Approach: tools/lcxl-leds.py, python3 stdlib only, standalone. It works with Pulsar
      closed and is scriptable from gig-up. --map [TRACK] paints by ROLE derived from the
      `^NN` bindings the file actually contains (dark = unbound on this track, which is
      the biggest cognitive win); --map with no track paints the channel CONVENTION, so a
      boot can never leave the board dark. --watch is the touch-reactive daemon: it keeps
      a model of every control and repaints from that model, so persistence is the data
      structure rather than a feature bolted on. --test walks every index so a dead LED
      is visible. --dry-run prints hex and sends nothing.
      
      It NEVER sends a CC. Not one, ever. SysEx out only; input is read-only via aseqdump
      as a child process, which does not steal MIDI from SuperCollider. Sending CCs would
      move live audio parameters, and CC 77-84 are MIDI-learned to Ardour track gains
      (CC77 down = total silence).
      
      Three things this cost, worth writing down:
      
      * TRANSPORT. `amidi -p hw:2,0,0 -S ...` is what lit knob A1 by hand — but only
        because nothing else held the raw device. On a LIVE rig the ALSA sequencer layer
        owns the rawmidi substream and amidi dies with "cannot open port hw:2,0,0: Device
        or resource busy". The tool that works on a cold rig failed on the only rig that
        matters. Primary transport is now `aseqsend` to the LCXL's writable SEQUENCER
        port, which coexists with SuperCollider; amidi stays as fallback.
      * PARSING. Splitting the .tidal on blank lines is right in principle (that IS
        Tidal's block separator) but stripping full-line `-- comments` first manufactures
        blank lines that cut a dN block in half: on vague_de_crime that put 9/9 bindings
        in the "fx" fallback because no segment head ever matched dN. Segment boundaries
        are now original blank lines PLUS every dN line, comments stripped only for the
        CC scan. Same track now reads rhythm=9, fx=6; gimme_acid 21 controls across
        rhythm/bass/fx; perfect.tidal 25 across rhythm/bass/lead/fx.
      * THE SEAM. The first --watch regexes assumed bare space-separated numbers and
        would have matched NOTHING — a daemon that runs clean, logs nothing and paints
        nothing. Pulled aseqdump's real format strings out of the binary ("Control change
        %2d, controller %d, value %d") and verified the whole loop by putting a fake
        aseqdump on PATH: 9/9 synthetic events decoded, coloured and sent, plus a
        respawn-with-backoff when the child exited.
      
      Also: the periodic re-assert runs on its own thread, not inside the read loop —
      inside, it would only ever fire when an event arrived, i.e. never during the
      silences when a device hiccup would actually go unnoticed.
      
      Verified: hex reviewed byte-by-byte against the protocol, 40 index/value pairs in
      one write, all bytes < 0x80; sends return rc=0 to the real device; --watch attaches
      and survives; event decode unit-checked (CC13@64 -> green full, CC49@0 -> dark red
      = LPF hard down, CC33@127 -> green flash, note 73 -> red flash). UNVERIFIED: what
      the panel actually looks like — nobody has eyes on it. Absence of an error is not
      evidence of a lit LED.
      PLN (Algolia) authored
    • fix(desire): one misplaced paren silenced all nine orbits — and a lens that can see a filter · cfa56b69
      desire.tidal measured 0/9 orbits: every declared channel SILENT AT SOURCE, master
      bus at -inf. Not the sample-buffer bug (b4f62757 fixed that), not a fader — the
      file simply never compiled.
      
          off 0.125 (|+ note 12 . (|* gain 0.6))
      
      `.` binds tighter than the section, so this parses as `|+ (note 12 . (|* gain
      0.6))` — composing a function into a ValueMap. GHC's own words: "'(.)' is applied
      to too few arguments". The fix is one pair of parens: `(|+ note 12) . (|* gain
      0.6)`. After it, 9/9 orbits measure STEADY through SC and Ardour.
      
      WHY IT COST A WHOLE TRACK. desire.tidal has ZERO blank lines, and a blank line is
      Tidal's block separator — so the file is ONE block, and a block either compiles
      entirely or not at all. One character took nine orbits. This is the third track
      this week where "the rig is silent" was really "the code did not compile", and it
      was only visible because the editor stopped deleting the word "error" from GHC
      messages (fork commit 374d8a3): PLN read the EVAL ERROR notification off his
      screen and forwarded it. Blank lines between orbits are free gig insurance.
      
      Also lands tools/control-lens.py, the instrument for the other half of the
      question. probe-chain proves an orbit makes LEVEL; it cannot prove a control
      WORKS, because rms is blind to a DJ filter or a bit-crusher — those move timbre
      at roughly constant loudness. So control-lens measures the right quantity per
      control kind: spectral centroid + band energies for filter/crush, rms for
      gain/mute, onset rate for ply/density. It drives the CC through the same path a
      physical knob takes (aseqsend -> SC MIDIFunc.cc -> /ctrl -> sStateMV, reused from
      lcxl-init rather than reinvented), captures before and after, and refuses to
      guess: a contaminated tap is DISCARDED, a silent baseline is SKIPPED (else one
      dead fader reads as twelve dead knobs), and "no measured change" is reported as
      NO CHANGE, never softened to "subtle". It also hard-refuses CC 77-84, which are
      MIDI-learned to Ardour's track gains.
      PLN (Algolia) authored
    • docs: the performing session is 'Tidal Live', not 'Tidal Multi' · 3a94a97a
      PLN: 'wtf tidal multi? we made everytihng on tidal live remember that please'. He was
      right and this line was the source of the error: check-mix.py had hardcoded the Tidal
      Multi path, so it parsed a stale archive session and confidently reported five faders at
      -inf that were actually up in the live session. A mixer auditor reading the wrong mixer is
      worse than no auditor.
      
      Both sessions are real — Tidal Multi still holds the ~398 G of historical per-orbit
      recordings — so this documents the distinction rather than renaming one away.
      PLN (Algolia) authored
    • docs(tasks): archive #26 — the silent-orbit hunt, and why the fix was one line in our own preload · 5ee9395c
      Written for a cold reader and for the blog: the two-part mechanism (upstream header-only
      reads + our own flag left flipped), why whitelist-by-mtime made it look haunted, the
      count assertion catching its own counting bug on the AppleDouble twins, the 'Required
      0 MB' red herring, and the two harness failures (zsh word-splitting, an early readiness
      gate) that cost more time than the bug itself.
      PLN (Algolia) authored
    • feat(probe): measure level OVER TIME — an aggregate cannot see a fade · 0ae4116a
      PLN, after I misdiagnosed a decaying orbit as a sparse one: "do you lack tooling to
      detect these over time? do you just listen to the loops once lol". Yes, and he was right.
      
      THE MEASUREMENT BUG. probe-chain reported ONE peak/rms pair per orbit over the whole
      capture window. That aggregate is mathematically incapable of showing a slope, and on
      this rig the slope IS the diagnosis. d7 in vague_de_crime measured peak -22.5 / rms
      -68.2 dBFS in a 14s window, and I read the 46 dB crest factor as "a sparse stab per
      cycle, just buried in the mix". It was nothing of the kind: it was a pattern sounding
      for ~3s and then dying, i.e. the `dN` = `xfade N` tail of the OUTGOING pattern while a
      SILENT new one faded in. PLN heard it correctly from the first sentence — "I hear it on
      ctrl+enter, then it fades in the back over 3s" — while my instrument said "quiet".
      
      A sparse part and a dying part produce the SAME aggregate: low rms, one lone peak. That
      ambiguity is not a judgement call to be made more carefully next time; it is missing
      information, and the fix is to stop throwing the time axis away.
      
      WHAT THIS ADDS. `probe-chain.py -t [BIN_S]` bins each capture (default 1s) into per-bin
      rms and prints a sparkline plus a classification:
      
          DIED      loud at first, then silent for the tail -> xfade tail, the eval did NOT take
          DECAYING  >12 dB downward trend -> almost certainly an xfade tail, not a part
          RISING    fading in
          STEADY    a real, sustained part
      
      Rendered as blocks with silence as a space, so a die-off reads as a visibly empty tail
      instead of a number you have to interpret. It reuses the existing verified tap machinery,
      so it inherits the contamination guard from 872676a2 — a row marked UNRELIABLE gets no
      timeline, because a fabricated slope would be worse than a fabricated level.
      
      VALIDATED TWICE. First against synthetic series, to prove the classifier can actually
      separate the two cases that fooled me:
          xfade tail  [-12..-45,-inf x7]      |▇▆▆▅▄       |  -> DIED
          sparse part [-20,-55,-19,-58,...]   |▆▂▆▂▆▃▆▂▆▂▆▂|  -> (spiky, no dead tail)
          steady part [-18..-20 flat]         |▆▆▆▆▆▆▆▆▆▆▆▆|  -> STEADY
      Then on the real rig, on the very orbit that caused this: d7 over 18s in 1.5s bins,
      |▅▆▅▅▆▅▆▅▅▅▆▆|, STEADY -25.0 -> -23.4 dB. Flat. Confirmed by PLN's ears in the same
      minute ("now its sticking i hear d7 stay").
      
      ALSO HERE: check-mix.py no longer audits the wrong mixer. It hardcoded
      "Tidal Multi/Tidal Multi.ardour" while the session in use is "Tidal Live" — so it
      confidently reported five faders at -inf that were actually up, a stale file audited as
      if it were live. It now resolves the session from the running Ardour process's own argv
      (matching on cmdline, because the binary is ArdourGUI and `pgrep ardour` finds nothing —
      a detail that already caused one wrong conclusion), falling back to Tidal Live by name.
      A mixer auditor reading the wrong mixer is worse than no auditor.
      
      Standing caveat unchanged and worth repeating: check-mix reads the session AS SAVED, so
      unsaved fader moves are invisible to it. Same family as the trap found minutes later —
      Pulsar evaluates the editor BUFFER, not the file on disk, so a grep of the file can
      disagree with what is actually playing. Verify against the layer that is live.
      
      Refs #26, #62, #65.
      PLN (Algolia) authored
    • fix(preload): the eager-preload existed — it just disabled the safety net it replaced · b4f62757
      THE SILENT ORBITS WERE A TWO-PART INTERACTION, AND PART TWO WAS OURS.
      
      Symptom, for two evenings: orbits went silent mid-set. Which ones changed every
      boot. The boot log looked perfect — every bank registered with the right file
      count — and 19741 `Buffer UGen: no buffer data` messages piled up in a single 6h
      session where no performer would ever see them.
      
      Part one is upstream design. `~dirt.doNotReadYet = true` registers banks from WAV
      HEADERS ONLY; the audio is read on first use. That read is fired asynchronously by
      `readFileIfNecessary` (DirtSoundLibrary.sc:268-274), which returns immediately — so
      the synth for the very event that triggered the read gets a bufnum with no samples
      in it. A correct file count in the log proves nothing about playability, because
      the count comes from the header.
      
      Part two is ours, and it is the one that turned a first-hit glitch into a dead
      orbit. `preload.scd` — our own generated warm-up — set `~dirt.doNotReadYet = false`
      and NEVER SET IT BACK. `readFileIfNecessary` has exactly one caller, and that call
      site is gated on `doNotReadYet` (DirtSoundLibrary.sc:259-261). With the flag left
      false, a bank registered header-only at boot is never read. Not read late: never.
      So every bank the whitelist happened to MISS was permanently silent, forever.
      
      And the whitelist missed banks routinely, because gig-up.sh built it from the N
      most-recently-EDITED tracks (`--last`). That is a guess about what will be played.
      the_revolution_will_be_sampled had not been edited recently, so `like_sugar` and
      `the_revolution` were never warmed — which is precisely the set of banks feeding
      the six orbits that were dead. Whitelist-by-mtime also explains the symptom that
      made this look haunted: the misses changed as files were touched, so a different
      set of orbits died on each boot.
      
      Neither half is fatal alone. A miss with the flag restored is a slow first hit; a
      flipped flag with a complete whitelist is invisible. Together they are a
      silent-orbit generator.
      
      THE FIX, in the generator that emits preload.scd:
        - Restore `doNotReadYet = true` after warming. This is the load-bearing line: it
          demotes the whitelist from load-bearing to a mere optimisation, which is the
          only thing it is safe to be on stage.
        - Drive the whitelist from an explicit setlist file (setlist_opal2026.txt) rather
          than from mtimes. Warm what will be PLAYED. `--last` stays for jamming.
        - Emit a per-bank COUNT ASSERTION. Re-loading a bank REPLACES it (addBuffer
          defaults appendToExisting = false, DirtSoundLibrary.sc:42-46), so this is
          index-stable and `like_sugar:21` keeps its meaning; the assertion guards the one
          case that would break that, a whitelist gone stale against the folders.
        - gig-up.sh writes via a temp file, because `> preload.scd` truncates BEFORE the
          generator runs — a failure used to leave an empty preload the boot read as
          "nothing to warm".
      
      The assertion earned its keep on its first run, failing `jbk_kick: expected 508,
      got 254`. Exactly 2x, and the tool was wrong, not the rig: that folder symlinks
      into the rhadamanthe pack, which carries an AppleDouble `._X.wav` beside every real
      `X.wav`. `glob('*.wav')` matches both; SuperDirt's `pathMatch("*")` skips dotfiles;
      `ls` hides them so the folder looks clean. Counting now skips dotfiles.
      
      VALIDATION — instrumental, no ears (PLN at work), two timepoints, per the rule that
      short windows make a time-varying fault look static:
        BEFORE: the_revolution_will_be_sampled, d4/d5/d9/d10/d11/d12 all -inf at source.
        AFTER : all 11 declared orbits carry signal, and STILL carry it on a second probe
                ~95s later with no re-eval (levels stable to ~0.2 dB).
        no buffer data since the eval: 0  (was 19741 in 6h)
        Preload: 47/47 banks pass the count assertion, 2352 files, 2.2 s, 1.2 GB.
        Also fully green: perfect 12/12 orbits, gimme_acid 10/10, mafia_sans_serif 7/7,
        wap 5/5. Bare `s "like_sugar*4"` / `"the_revolution*2"` with no helpers: audible.
      
      STILL BROKEN, SEPARATELY: vague_de_crime is silent at source with ZERO buffer
      errors, and every one of its dead orbits routes through the ^NN helpers
      (gMute2/gM1/gM3/gF1/gF3). That is #61 — an untouched `^NN` evaluates to `silence`,
      not 0 — not this bug. Fixing the buffers removed the noise that was hiding it.
      
      Refs #26, #63. Surfaces #61. Note for #44: the readiness gate passes BEFORE the
      preload finishes, since "listening to Tidal" is printed by ~dirt.start well ahead
      of the warm-up.
      PLN (Algolia) authored
    • docs(tasks): archive the silent-orbit hunt — two evenings, one header-only buffer · 9d5d4257
      Two entries, both written as documentary source rather than bookkeeping.
      
      #63 is the epidemic itself: `~dirt.doNotReadYet = true` makes SuperDirt read WAV
      HEADERS ONLY, so every bank registers with a correct file count and its buffers
      hold no audio. The methodological lesson is worth more than the bug — every
      measurement used a 4-8 second window, and at that timescale a TIME-VARYING
      failure looks STATIC. "These two sample banks are dead" was overwhelming and
      wrong, and six hypotheses got built on top of it. The decisive test was one
      command: same pattern, t+3s healthy, t+26s gone.
      
      Also recorded honestly: our own probe tool FABRICATED signal from contaminated
      taps and contradicted PLN's ears for an hour. Five orbits at an identical +0.0
      dBFS peak was the fingerprint nobody read. Kept in the archive on purpose — a
      tool that invents evidence outranks the human in an argument it should always
      lose, and that is worth remembering out loud.
      
      #59 is the smaller, funnier one: "sound broke after a Window Reload, fixed by a
      Tidal reboot" was never a breakage. Lazy activationHooks left the package
      dormant, and `tidalcycles:reboot` was simply the first activationCommand that
      woke it. The workaround worked for a reason unrelated to the story told about it,
      which is exactly why nobody looked further for months.
      PLN (Algolia) authored
    • fix(probe-chain): refuse to report signal the tap was not actually measuring · 872676a2
      The tool invented signal, and the invented signal sent an entire investigation
      down the wrong road.
      
      A run on 2026-07-28 came back with five different orbits at an IDENTICAL +0.0
      dBFS peak. Twelve independent orbits do not peak at exactly full scale together
      — that was one shared source (the mix) being measured five times, because the
      capture nodes had acquired PipeWire links nobody asked for. `--target 0` is
      supposed to stop pw-record auto-connecting to the default source, and usually
      does, but not reliably.
      
      The consequence was worse than a missing number. The contaminated table said
      d4 and d5 had "recovered after seeding", so that got reported as a result, while
      Ardour's own meters showed no input on those tracks at all. PLN was looking at
      the truth and being told otherwise. A measurement tool that fabricates signal is
      not a tool — it is a confident liar, and it outranks the human's ears in an
      argument it should always lose.
      
      Two changes:
      
        * link() now CUTS every pre-existing link into the capture node before making
          its own, so an auto-connection cannot ride along.
        * verify() re-reads the graph AFTER the capture window and asserts the node was
          fed by exactly the two ports we intended. Checked after, not before, because
          an auto-connection can appear at any point while the node is alive — a setup
          -time check would still let contamination through.
      
      Rows that fail verification print "UNRELIABLE — ignore this row" INSTEAD of a
      diagnosis, plus a summary naming what each bad tap was really linked to. Refusing
      to answer is the correct behaviour here; a plausible wrong number costs hours.
      
      Validated by re-running against the live rig: the clean table (d1/d2/d3/d7 with
      signal, d4/d5/d9-d12 silent at source) matched what PLN could hear, where the
      contaminated one had contradicted him. Confirmed on the way that the SC->Ardour
      link mapping is exact (out_(2N-1)/out_(2N) -> ardour:Tidal NN for all 12), so the
      disagreement was never routing.
      PLN (Algolia) authored
    • feat(rig): tidal-remote — the CLI that finally closes the diagnostic loop · d3de9bd5
      Companion to the `remote` channel just added to pulsar-tidalcycles. With this,
      the whole cold-start-and-measure sequence runs unattended:
      
          tools/tidal-remote.py boot live/midi/nova/jazz/the_revolution_will_be_sampled.tidal
          tools/probe-chain.py -s 6
      
      `boot` encodes the one ordering that actually works, which was learned the
      expensive way:
      
          reboot  ->  wait for BootTidal.hs  ->  seed the surface  ->  eval
      
      Every step of that order is load-bearing. Tidal's control map (`sStateMV`, where
      `^NN` values live) is state INSIDE the ghci process, so a reboot empties it —
      seeding before a reboot is wasted work. And evaluating before seeding is the
      mute-bomb from #61: an untouched `^NN` resolves to `silence`, not 0, so any
      stream referencing an unmoved control is completely silent with no error printed
      anywhere. Getting these three steps in the wrong order is precisely how an
      evening went into "d4/d5/d9-d12 make no sound".
      
      Two things it refuses to do:
      
        * It does not claim success. UDP sends land in the void whether or not anyone
          is listening, so `channel_open()` tests the port by trying to BIND it — if
          the bind succeeds, nobody was there, and the command fails loudly instead of
          printing a cheerful nothing. A remote that reports its own success is how you
          end up with a green check on a silent rig.
        * It does not infer readiness. Every path ends by telling the caller to go
          measure with probe-chain. The rig has taught us repeatedly that static
          self-reports agree with each other while the audio disagrees with all of them.
      
      OSC encoding is hand-rolled (~10 lines, all-string args) so this stays
      dependency-free — it has to run from a bare launch script, where a pip install is
      not part of the plan. Verified by round-tripping an encoded message back through
      a decoder: 64 bytes, 4-byte aligned, address and typetag and args all recovered.
      
      Also resolves track names loosely (absolute path, repo-relative, or bare name
      searched under live/ and copycat/), erroring on ambiguity rather than guessing —
      the corpus has genuine duplicate basenames and picking one silently would be
      worse than asking.
      PLN (Algolia) authored
  2. 27 Jul, 2026 9 commits
    • feat(rig): measure the chain, manage the server, seed the surface — three tools… · 3f81e14d
      feat(rig): measure the chain, manage the server, seed the surface — three tools that end the silent-orbit era
      
      Tonight cost an evening to "d4/d5/d9-d12 make no sound", and the reason it cost
      an evening is that every layer was inspected by READING and none by MEASURING.
      PLN's correction — "this is a great tidal-ears case, observe the audio streams"
      — was right, and the measurement took six seconds.
      
      ## tools/probe-chain.py — where does the signal stop?
      
      Taps the live chain at three points at once and reports peak+rms dBFS per orbit:
      
          SuperCollider:out_N/N+1   the orbit as SuperDirt renders it
          ardour:Tidal NN/out       after Ardour's fader/mute/plugins
          ardour:Master/out         the mix
      
      Taps are ADDITIVE PipeWire links into `pw-record --target 0` capture streams, so
      nothing existing is unlinked and the audio path is untouched — safe mid-set. The
      `--target 0` is the trick that makes it possible to tap a mid-chain node instead
      of only a device.
      
      Orbit->port mapping is DERIVED, not hardcoded: `~dirt.start(57120, [0,2,4,...])`
      means orbit k writes bus 2k, and PipeWire exposes bus b as out_(b+1)/out_(b+2).
      
      First run settled in one shot what four sessions of hypothesising could not:
      
          d1  -28.6 rms   d2  -42.5   d3  -37.4   d7  -21.5   d8  -29.8   <- audible
          d4 d5 d6 d9 d10 d11 d12 = -inf, absolute digital silence
      
      Exact match with what PLN could hear. **Ardour fully exonerated** — faders,
      mutes, routing, all of it. The signal never existed. Every hypothesis that had
      pointed downstream was wrong, and one reading killed them all.
      
      It reports peak AND rms deliberately: a lone peak can be one stray sample or a
      click, and judging an orbit "working" on peak alone is how you mistake a
      transient for audio.
      
      ## parvagues-sc.service — SuperCollider becomes visible
      
      The reason the real error was unreadable all evening: sclang's post window was a
      bare /dev/pts/3. Not readable by tooling, gone when the terminal closes, gone on
      a crash. The process was equally fragile — owned by a terminal, and killable by
      a stray probe (which is exactly what happened: a probe script of mine attached to
      the DEFAULT server, i.e. PLN's live one, and its `0.exit` sent `/quit`. `s` in
      sclang is the default server; changing the SuperDirt OSC port isolates nothing).
      
      As a systemd --user unit we get restart-by-command, survival across terminal
      close and sandbox teardown, one place for The Bridge to show status — and the
      post window in the JOURNAL, where it can be PARSED into readiness state instead
      of squinted at. Within a minute of it landing:
      
          like_sugar (22)   the_revolution (36)   diams_dj (16)
      
      — the "missing" banks were registered correctly all along, plus 106 real
      `File reading failed` warnings on `zz._`-prefixed files that had never once been
      visible. That is the #45 lead, handed over by a log we could finally read.
      
      `Restart=no` on purpose: a failing boot script under Restart=always would spam
      the MIDI graph and hide the failure. Audio gear should fail loudly, never flap.
      `QT_QPA_PLATFORM=offscreen` is load-bearing, not cosmetic — a Qt-linked sclang
      dies with the display stack, which is how an overnight suspend used to kill the
      music (d8346672).
      
      ## tools/lcxl-init.py — fix the mute-bomb at its actual root
      
      Verified against tidal-1.9.5 source rather than guessed: `"^51"` is `cF_ "51"` is
      `_cX_ getF "51"`, and `_cX_` returns **`silence`** on a missing key. `#` cannot
      emit without a right-hand value, so any stream touching an unmoved control is
      silent — and `dN` being `xfade N` means you hear the PREVIOUS pattern's 4-cycle
      tail dying, which reads as a filter closing. That illusion is what cost the
      evening.
      
      BootTidal.hs's two mitigations are both weaker than this one. `orDef` reaches
      only the ~13 g* helpers — never the ~1860 inline `^NN` refs across 343 track
      files, nor arithmetic sites like `midiOn ("^34" - "^18")` where
      touched-minus-untouched is itself empty. The `setF` seed block is correct in
      principle (same `sStateMV` the OSC `/ctrl` handler writes — traced in Stream.hs)
      but depends on boot order and on that ghci having actually run it.
      
      lcxl-init instead emits real CC messages into the identical path a physical knob
      takes: CC -> SC's `MIDIFunc.cc` -> `/ctrl` -> Tidal's map. By construction
      indistinguishable from PLN sweeping the whole surface by hand — the one thing
      already known to work. It cannot diverge from reality because it IS the reality
      path, and it fixes helpers, inline refs and arithmetic sites at once, whatever
      state ghci is in. PLN's framing: "most things that are templated lack an init
      value" — so give them one, from the hardware side.
      
      Encodes the corpus convention `range <neutral> <extreme>`, so 0 is neutral for
      everything except the three DJ filters, whose neutral is the centre detent
      (64/127 -> lpf 20000 / hpf 20 = true bypass).
      
      D-row faders (77-84) are deliberately NOT seeded: they are MIDI-learned to Ardour
      TRACK GAINS, so blasting values would move Ardour's faders while the physical
      faders stay put — a surface/mix desync worse than the problem. `^77` is already
      covered by `orDef 0.769`.
      
      Two bugs found and fixed by verifying rather than assuming:
        * port resolution used `aconnect -i` (READABLE ports) and so returned SC's
          `out0` — a source. Sending to it goes nowhere. It must be `-o`, the writable
          destinations. Resolved BY NAME on every call, never cached: the LCXL has
          already moved 20:0 -> 24:0 across a replug, and a binding resolved once at
          boot then silently invalidated is this rig's most expensive bug class.
        * the first verification attempt tapped the destination port with aseqdump and
          saw 0 messages — but you cannot SUBSCRIBE to a destination. Delivery was
          then proven properly through Midi Through (which echoes in to out):
          controller 49 value 64 and controller 50 value 64 observed arriving. The
          send was always fine; the observation was broken. Report which one you
          proved.
      PLN (Algolia) authored
    • fix(rig): the yellow-LED loop was our own systemd service — and the "broken"… · 3d628ff4
      fix(rig): the yellow-LED loop was our own systemd service — and the "broken" orbits were closed Ardour faders
      
      Two independent silent failures, both found tonight, both invisible from the
      editor. Neither was in Tidal, which is where every hypothesis pointed.
      
      ## 1. The yellow-button loop: we were fighting ourselves
      
      Symptom, standing for weeks: paint an LCXL button a colour, press it, it snaps
      to yellow and wipes the colour. `aconnect -d 14:0 24:0` fixed it — and then it
      came back, every time, "after a Tidal reboot". We blamed SuperCollider's
      `MIDIIn.connectAll`, Ardour's control surface, WirePlumber.
      
      It was `~/.local/bin/midi-autoconnect.sh`, a service we wrote ourselves. Its
      rule 2 connected `Midi Through:0 -> Launch Control XL:0`, commented
      "LED/visual feedback", and re-asserted it every 2 seconds. That IS the loop:
      
          press -> LCXL emits Note On velocity 127
                -> LCXL:0 -> Midi Through:0 -> (rule 2) -> LCXL:0
                -> LCXL reads an inbound note velocity as an LED COLOUR
                -> 127 = 0x7F = full red + full green = YELLOW
      
      Not "reboot recreates it" — a 2-second reconcile loop was silently undoing the
      operator's fix within two seconds of every manual cut. The rule bought nothing:
      the HUD drives the LEDs directly by SysEx to the LCXL's own port.
      
      Rule 2 is now INVERTED — it disconnects instead of connecting, so the return
      leg is actively severed rather than merely not created (Ardour and SC can both
      recreate it independently; same shape as every other binding on this rig — one
      that must be re-asserted, never assumed). LCXL -> Midi Through -> Ardour still
      works, so click-to-map fader learn is unaffected.
      
      Rule 3 (Midi Through -> every aseqdump) is deleted too: aseqdump clients
      already subscribe to the LCXL directly, so the extra hop delivered every CC
      TWICE to the HUD — duplicated work on the renderer we are trying to keep off
      the audio thread's back (see the typing-crackle investigation).
      
      Verified: after `systemctl --user restart`, `Connecting To` on client 14 no
      longer lists 24:0, and stays that way across reconciles.
      
      ## 2. tools/check-mix.py — the mixer is part of the signal path
      
      An evening went into "d7 plays one bar then fades to nothing" and "d9-d12 make
      no sound, I see the meter move but nothing on master". We chased the
      untouched-control mute-bomb, the DJF helpers, orbit routing, a missing sample
      bank. Measured and eliminated, in order: PipeWire links (all 24 present,
      out_17->Tidal 09 ... out_23->Tidal 12), Ardour route outputs (all 12 active and
      connected to Master), the sample bank (like_sugar present, 22 files, `:21`
      valid, inside a loaded glob since April), and the helpers themselves
      (check-boot.sh pass 3: all 13 audible against an empty control map).
      
      Then: **Tidal 02, 06, 07, 08 and 12 have their faders at -inf dB.** d7 in
      bombe_dj is Tidal 07. d12 in the_revolution is Tidal 12. Exact match.
      
      Why it cost a whole evening rather than a glance:
        * Ardour meters are POST-fader by default, so a shut fader shows NO meter —
          the display agrees with the "Tidal is broken" theory instead of refuting it.
        * `dN` is `xfade N`, so a re-eval fades the OUTGOING pattern out over ~4
          cycles. With the fader shut you hear the previous pattern dying: "plays a
          bar then slowly goes to zero", which reads exactly like a filter closing.
        * The faders are MIDI-learnable from the LCXL, so a physical fader left down
          persists into the saved session and outlives every reboot of everything.
      
      The tie-breaker: `d12` in the_revolution uses no `g*` helper and no `^NN` at
      all. Such a pattern cannot be silenced by anything inside Tidal — so its
      silence proved the fault was downstream, and the fader was the only downstream
      thing left unverified. A control case with no confounders beat four sessions of
      hypothesising.
      
      check-mix.py reads the session XML and flags, per Tidal route: fader at/near
      -inf, muted, or not routed to Master. Exit 1 so `gig-up.sh` can gate on it.
      It states its own limitation rather than implying more than it checks: the
      session is read from DISK, so a clean report means "boots healthy", not "the
      running mixer is healthy". Live verification needs Ardour's OSC surface, not
      yet wired.
      PLN (Algolia) authored
    • test(boot): check-boot now RUNS the helpers against an empty control map (#55) · fdcf185a
      PLN asked what tooling would have caught tonight's mute-bomb. Honest answer:
      nothing we had, and not tidal-ears — it analyses audio, and this bug produced
      SILENCE. Silence is indistinguishable from silence no matter how well measured.
      The defect was symbolic: a pattern emitting zero events.
      
      Typechecking could not catch it either. `gF1 $ sound "bd"` is perfectly
      well-typed while producing NO EVENTS, because an untouched "^NN" yields nothing
      and `#` cannot emit without a right-hand value. Pass 1 of this script was green
      throughout the entire outage.
      
      The missing capability was the ability to ask one question mechanically:
      "what does this helper do with an EMPTY control map?" — i.e. on a fresh boot,
      before any knob has moved. That is now pass 3: it extracts the helper block,
      appends assertions, compiles, and RUNS them, checking that each of gF1/gF2/gF3,
      gMask, gMute1-3, gM1-3, gPanic, midiGdef and a composed gF1.gM2 stack all emit
      events with `State (Arc 0 1) M.empty`.
      
      It is a real execution rather than a typecheck, but pure pattern evaluation
      only: no stream, no scsynth, no MIDI, no port binding. Safe to run mid-set.
      
      Verified in BOTH directions, which is the whole point:
        - current BootTidal.hs        -> 13/13 audible, exit 0
        - orDef guards stripped out
          (i.e. the pre-3685c743 code) -> 11 FAILs naming each silent helper, exit 1
      
      That negative test also confirmed something useful about the shape of the bug:
      gPanic and midiGdef stayed audible even with the guards stripped, because they
      route through `range`/arithmetic that survives an empty map differently from
      `#`. So a partial fix would have looked partially fine — exactly the kind of
      half-green result that lets a bug like this hide for months.
      
      Turns the ad-hoc Haskell probe written during the incident into a permanent
      regression test. This bug class has now bitten three times (#21 djfbus, #48's
      one-directional filters, #55's untouched-control silence); pass 3 is the first
      thing that would have caught any of them before a boot.
      PLN (Algolia) authored
    • fix(boot): an UNTOUCHED "^NN" yields no events, not 0 — the real mute-bomb (#55) · 3685c743
      The bug behind "no sound on d5 in perfect.tidal, meter moves, master silent,
      plays a bar then slowly fades to zero, then crackles". Also, finally, the
      true root of #21 — which was diagnosed as a missing `djfbus` and fixed as
      "safe at rest (0)", when the actual defect was one layer deeper.
      
      FINDING, proven by queryArc against an empty controls map (not by ear):
      
          gF1 $ sound "bd", knob never touched  ->  SILENT (no events)
          gMask  untouched                      ->  SILENT
          gMute3 untouched                      ->  SILENT
          gain ("^77" * 1.3) untouched          ->  SILENT
          gPanic untouched                      ->  SILENT
      
      A bare `"^NN"` whose CC has never arrived does NOT default to 0 — it yields
      NO EVENTS AT ALL. And `#` (i.e. `|>`) cannot emit without a value on its
      right-hand side, so the entire stream disappears. Every g*-wrapped stream in
      the corpus is therefore silent until that physical control is moved once.
      
      This is why "move all the controls" was always step one of a gig, and why
      PLN's rig felt fine for months: the habit was masking a mute-bomb. Today's
      BootTidal restart (to pick up the 77903244 DJF fix) wiped the control map and
      re-armed it across the whole surface at once — so the fix appeared to make
      things worse.
      
      It also explains the odd "plays one bar then slowly fades to zero": `dN` is
      `xfade N`, which crossfades the outgoing pattern out over 4 cycles while
      fading the new one in. The new one had no events, so what PLN heard was the
      OLD pattern dying gracefully into nothing — meter moving throughout, master
      quiet once the fade completed. The symptom looked like a filter closing;
      it was a crossfade to silence.
      
      TWO-LAYER FIX, because one layer cannot reach everything:
      
      1. `orDef` in the helper let-block — substitutes a default when a control
         pattern is empty. Applied to gDJF (0.5 = centre = true bypass),
         midiOn/midiOff/midiNo/midiBoth (0/1/1/0 = "not pressed"), midiGGlobal
         (0.769, so *1.3 == unity rather than 0), and gPanic (0 = normal, not
         silent). It must live at the HELPER level, not per call site, because
         tracks combine controls arithmetically — `midiOn ("^34" - "^18")` in
         perfect.tidal:63 — and `touched - untouched` is itself empty, so a
         per-CC default could never catch it.
      
      2. Boot-time control seeding via `setF` — the corpus-wide half. `orDef`
         cannot reach the 1860 sites across 343 track files that apply controls
         directly (`# crushbus 41 (range 16 3.5 "^53")`). Editing those is
         #46-scale work. Seeding every LCXL control at boot fixes all of them at
         once while touching no track file; a real CC overwrites its seed the
         instant the knob moves.
      
      Seed policy follows the authoring convention `range <neutral> <extreme>`,
      so 0 == neutral: A/B knobs 0, C1-C3 0.5 (DJF centre), C4-C8 0, fader 77
      0.769 (unity), faders 78-84 1.0 (gains open = audible), all buttons 0.
      
      Seed coverage was verified by set-difference, not assumed: 49 seeded vs 54
      distinct "^NN" in live/. The 5 stragglers were each resolved rather than
      waved off — ^45 needs no seed (it flows through midiOn, so orDef covers
      it), ^6 and ^61 are commented out at their only call sites, and ^1 (an
      external keyboard modwheel) and ^21 were added explicitly.
      
      Verification of the fix, all four knob positions, via an explicit controls
      map — untouched now bypasses and NO touched behaviour changed:
      
          untouched -> cutoff=20000 hcutoff=20     (was: SILENT)
          at 0.0    -> cutoff=180   hcutoff=20     (low-pass to subbass)
          at 0.5    -> cutoff=20000 hcutoff=20     (bypass)
          at 1.0    -> cutoff=20000 hcutoff=8000   (high-pass to superhigh)
          gMute3 pressed -> SILENT                 (mute still mutes)
          gPanic armed   -> gain 0.0               (panic still panics)
      
      tools/check-boot.sh gains a second pass over the seed block (pinning the
      `streamSetF :: Stream -> String -> Pattern Double -> IO ()` signature it
      calls through), so neither half of this can rot silently. Its own first
      attempt at that pass was itself broken — a sed that produced
      `_seed = _seed = concat` — and the checker caught it. Same lesson as
      77903244: run the guard, don't trust that you wrote it correctly.
      
      prelude.tidal (the stale-boot rescue sheet) carries the matching orDef form
      plus a ready-to-eval `_seedNow` list, for the case where a gig laptop boots
      silent and the cheapest fix is one paste rather than sweeping 40 controls.
      PLN (Algolia) authored
    • fix(boot): the DJF "fix" was a parse error — BootTidal.hs could not load at all · 77903244
      Mid-rehearsal, PLN reported the three DJ filters still misbehaving after
      yesterday's #48 bidirectional rewrite. He described three symptoms:
      
        C1 centre->0  : no LPF effect on d3 (hats)
        C1 centre->0  : kick goes from TOTALLY high-passed to normal
        C1 centre->127: d3 high-passes
      
      Those fingerprint the *previous* (2026-07-25) definition exactly —
      `gHPF ch = (# hpf (range 20 8000 ch))`, one-directional, no lpf term:
      at centre hpf=4010Hz (kick gutted), at 0 hpf=20Hz (normal), at max
      hpf=8000Hz. Four observations, four matches. So the running ghci was not
      executing the new code. The question was why.
      
      Root cause: the new gDJF was written as a right section containing another
      operator of the same fixity —
      
          gDJF ch = (# lpf (...) # hpf (...))     -- `#` is infixl 9
      
      which is a hard Haskell parse error, not a subtle type slip. GHC:
      "The operator '#' of a section must have lower precedence than that of the
      operand". One bad line takes down the entire enclosing `let` block, i.e.
      EVERY g* helper — gF1/2/3, gMask, gMute*, gPanic, the drum helpers.
      
      What made it dangerous rather than merely broken: nothing surfaced it. The
      live ghci still held the older definitions from before the edit, so the rig
      kept making sound and behaving plausibly while the file on disk was
      unloadable. The failure was scheduled for the next cold boot — i.e. on
      stage at OPAL. Same shape as #21's mute-bomb: a definition that lives only
      in a running process is not a verified definition.
      
      Fix: express it as two composed sections, which is also semantically
      cleaner (two independent unions rather than one nested):
      
          gDJF ch = (# lpf (...)) . (# hpf (...))
      
      The filter math itself was correct and is unchanged — verified at three
      points: v=0 -> lpf 180 / hpf 20 (low-pass closed to subbass), v=0.5 ->
      lpf 20000 / hpf 20 (both wide open = true bypass at the centre detent),
      v=1 -> lpf 20000 / hpf 8000 (high-pass climbed to superhigh). Applied to
      both BootTidal.hs and the live/lib/prelude.tidal rescue sheet, which had
      inherited the identical broken form.
      
      Also adds tools/check-boot.sh so this class of bug cannot ship again: it
      extracts the helper let-block, dedents it into a real module, stubs the two
      stream-bound names it closes over (p, setI), and runs `ghc -fno-code`.
      Types only — no codegen, no stream, no port 6010 grab, safe to run mid-set.
      
      Validation, both directions:
        - real BootTidal.hs      -> OK, exit 0 (whole 164-431 block typechecks
                                    against tidal-1.9.5 / ghc 9.4.7)
        - bug reintroduced       -> FAIL, exit 1, pointing at the exact line
      
      That negative test earned its keep immediately: the first version of the
      guard used `ghc | grep -q` under `set -o pipefail`, where a failing ghc
      makes the pipeline non-zero regardless of grep's verdict, silently
      inverting the check. It reported OK on a file it had just proven broken.
      Now it runs ghc once and tests the captured exit code. A guard that has
      never seen the bug it targets is a hypothesis, not a guard.
      PLN (Algolia) authored
    • chore: save before claude · a1e860a1
      PLN (Algolia) authored
    • chore(gig): checkpoint live-session track edits before harmonization pass · 9ad43e8f
      Rehearsal/play session on the UMC rig (SOF->UMC migration validated end to
      end tonight): electric_hammer, bombe_dj, ete_a_mauerpark, vague_de_crime,
      gimme_acid, take_5_drops tuned; raph collab tracks (desire, mafia_sans_serif,
      piment_bresilien) touched; backlog.md and armada calibration catalogs
      regenerated alongside.
      
      Committing as a clean checkpoint before starting the d1-d12 channel
      harmonization + LCXL fader remap (task #46) and the sample/synth reference
      cleanup (task #45), so those land as reviewable diffs against a known-good
      base rather than mixed in with tonight's live tuning.
      PLN (Algolia) authored
    • docs(gig): archive the audio-path rescue — three tasks, one recurring failure shape · 164f8b44
      Archives #38, #39, #40 with full learnings, per the document-as-you-go discipline.
      
      The thread: the onboard SOF codec stopped answering IPC mid-session and took the
      whole audio stack down with it — WirePlumber wedged in uninterruptible D-state
      inside ipc3_tx_msg_unlocked, unkillable by SIGKILL, scsynth dead, reboot the only
      exit. That escalated the UMC202HD from "nice upgrade" to "resilience fix", and the
      move onto it is now done: default sink switched, the SOF internal DMIC (card3
      pcm4c, the exact PCM that hung) disabled by a scoped WirePlumber rule that leaves
      all five SOF playback sinks alone.
      
      The archive's real value is the pattern that runs through all three entries. Every
      failure this session had the same shape: a binding resolved once at startup,
      invalidated by a device event, failing with NO error surfaced. Ardour's master port
      (twice — it restores its own saved ports and ignores the default sink, then loses
      the link entirely on a wireplumber restart). SuperCollider's cached MIDIOut after a
      replug moved the LCXL 20:0 -> 24:0. The HUD's aseqdump, still ALIVE on the dead
      port, which is why its respawn-on-death recovery never fired — liveness is not
      health. And the session layer itself: after an overnight suspend the UMC
      re-enumerated 118 times, came back healthy at the hardware level, and WirePlumber
      simply never rebuilt its node.
      
      Also recorded: a wireplumber restart resets sink volume to the device default (87%),
      which turned a routine verification tone into a painfully loud one. Attenuate before
      making sound, always.
      
      Corrected doctrine while here — commits belong on master by default. PLN is a lone
      dev who reviews his own work; branches are for cloud agents, parallel sessions and
      genuine prototypes. The memory claiming otherwise was wrong and cost real time.
      PLN (Algolia) authored
    • feat(lcxl): drive the LEDs over SysEx — the Note-On path was never going to work · 1459ef02
      The LED work (#11) had been stuck for two days on "some nonzero value lights it",
      with no reliable palette and buttons that refused to light at all. Today's live
      session found out why, and it was not a bug in our code.
      
      THE ROOT CAUSE. The Launchpad Note-On protocol only lights a control if the
      CURRENTLY SELECTED template maps that exact note/CC on that exact MIDI channel.
      On User 1 the LCXL's buttons are CC-mapped, so every Note-On we sent to notes
      73/74/91/92 had no matching target and was silently discarded. The knobs are
      note-mapped, which is why they lit and the buttons never did — a split that
      looked like flaky hardware and was actually the protocol working as documented.
      
      THE FIX. The SysEx "Set LEDs" message updates any control regardless of its
      note/CC value or MIDI channel:
      
        F0 00 20 29 02 11 78 <template> <index> <value> [<index> <value> ...] F7
      
      Verified on hardware: top button row went red and bottom row green on the first
      try, after Note-On had failed on those same buttons all morning. Indices are
      contiguous (00-07/08-0F/10-17 knob rows, 18-1F and 20-27 button rows, 28-2B
      side, 2C-2F arrows), which is far easier to generate role-family colours over
      than the scattered note numbers.
      
      PALETTE, MEASURED NOT GUESSED. colour = (16 * green) + red + flags, each channel
      0-3, flags 12 normal / 8 flashing. Confirmed by lighting the top knob row with
      the full ramp and having PLN read the colours back left to right: off, red low,
      red full, green low, green full, amber low, amber full, yellow — matching the
      Programmer's Reference table exactly. Also ran the built-in brightness tests
      (B0 00 7F / 7D): all-orange then all-dim-yellow, which proves the unit is NOT in
      low-power mode and that dim-vs-full is simply hard to read by eye. Conclusion for
      the design: use FLASH, not brightness, when a state must be unmissable.
      
      STALE-BINDING FIX. ~lcxlOut was resolved once at boot. A replug moves the ALSA
      port (we watched it go 20:0 -> 24:0) and the cached MIDIOut then points at a
      device that no longer exists — LEDs die silently while input keeps working. It
      now re-resolves lazily on every write, and drops the cache on error so the next
      write retries. Added ~lcxlReconnect for explicit post-replug recovery, kept
      manual on purpose: MIDIClient.init re-scans the whole MIDI world and is not
      something to fire mid-set on a guess.
      
      This is the fourth instance today of one failure shape: a device binding resolved
      once at startup, invalidated by a device event, failing with NO error. The others
      were Ardour's master port (twice) and the HUD's aseqdump sitting alive on a dead
      port. Tracked in #41.
      
      Validation: delimiter balance checked; full compile is proven at the next SC boot,
      not before — deliberately not restarting SuperCollider mid-rehearsal.
      PLN (Algolia) authored
  3. 26 Jul, 2026 1 commit
    • feat(slop): clip-idea planner — 7 OPAL tracks are cuttable today, and Slopmotion was never missing · 1b81e5ea
      Two findings, one of which unblocked a task that had been stuck on a false premise.
      
      THE FALSE PREMISE. visuals/slop/README.md said Slopmotion was "not installed here yet —
      this is the real blocker", after a thorough machine search on 2026-07-25. That was wrong.
      Kevin Granger IS Shipow (git log on the repo shows both identities authoring the same
      commits), so Slopmotion is hydra-live-hexa — a registered working directory of this
      workspace all along. Task #28 was blocked on nothing. The lesson worth keeping: resolve
      the PERSON before concluding the SOFTWARE is missing. README corrected in place, with the
      wrong claim quoted rather than silently deleted.
      
      WHAT THAT UNLOCKS. Everything needed to generate is already on disk, no network at all:
      764 loop mp4s across 23 playsets (including a `parvagues` pack of PLN's own imagery); a
      24-pack catalog in src/data/playsetPacks.json which is a generated snapshot of the online
      Supabase `packs` table; playwright 1.60 + chromium; and a working headless harness to copy
      in scripts/capture_fx_previews.mjs. This matters because the online route is CLOSED:
      shipow.io is NXDOMAIN, hydra-live-hexa.vercel.app returns DEPLOYMENT_NOT_FOUND, and .env
      carries only HEXA_LOGIN/HEXA_PASS with no VITE_SUPABASE_URL — so the DB can't be reached
      from here, and doesn't need to be.
      
      THE PLANNER. visuals/slop/plan_clips.py joins three sources to answer "which shareable
      clips can we make right now, for tracks actually in the OPAL set?" Answer: 7 of 13.
        - setlist from backlog.md § OPAL 2026, with MEASURED setcps from the BPM-arc doc rather
          than the backlog's annotations (which were wrong on three tracks).
        - recordings = Take89 (Montreuil Algorave V3, 2026-05-22): PLN-ear-validated boundaries,
          plus per-track peak-anchored 30s/90s promo_cuts already computed by the drops detector.
        - visuals = the playset catalog.
      Cuttable: WAP, Take 5 Drops, Piment Bresilien, Mafia, Sunshine, Desire, Revolution. The
      6 that aren't (Bombe, Doing it Right, Perfect, Gimme Acid, Vague de CRIME, Electric
      Hammer) are the newer/reworked tracks with no Take89 take — they need a fresh recording,
      which gig_record.sh will produce at the next rehearsal or at OPAL itself.
      
      Playset/FX pairing is driven by MEASURED features, not vibes: tilt_mean (spectral tilt)
      picks the dark/mid/bright playset family, kick_density_p70 decides whether the chain is
      trigger-heavy (jumpCut/vibration/flash on kick) or breathing (liquix/wetLens/slowmo), and
      ssm_novelty_max sets the Automix interval. Thresholds are the terciles of THIS set's
      observed range (tilt 0.13..0.54, kick 1.75..3.75), so they split real material rather than
      an abstract scale. The one frankly aesthetic layer — which pack names read as "dark" vs
      "bright" — is marked as such in the source.
      
      VALIDATION. Master-timeline offsets cross-check against an independent source: the planner
      derives Desire's track start from the studio bundle's boundaries_ground_truth.json and
      lands on 2926s, which is exactly the 48:46 cut in armada's separately-authored
      boundaries_take89_validated.json. Two files, two provenances, same number.
      
      Renderer (audio slice + playset -> mp4 via the real Hydra engine) is NOT built yet.
      PLN (Algolia) authored
  4. 25 Jul, 2026 17 commits
    • docs(gig): structured archive for finished OPAL 2026 tasks · 52a05f96
      The /pre-compact ritual archives completed board tasks somewhere durable before
      clearing them, so the learnings survive the task disappearing. armada/tasks/
      completed-archive.md already does this — but its header scopes it explicitly to
      L'Armada, the media/marketing toolbox. Gig prep is a different thread, and mixing
      two documentary trails in one file makes both worse to mine later. Same discipline,
      separate file.
      
      Seven finished tasks captured in the established description/done/learnings/deps
      shape: #15 runbook, #21 mute-bomb, #32 recorder, #19 disk archive, #9 launcher,
      #27 highlight jitter, #5 HUD version control.
      
      The learnings are the point, not the inventory. The ones worth re-reading:
      
      - #21: a fix that lives only in a running process is not a fix. The gF rewrite was
        correct and live-validated for a full day while still being one SuperCollider
        reboot away from silently rearming. "Live-confirmed" and "durable" are different
        states and the gap between them is where this class of bug hides. The deeper
        design lesson: make rest position == no effect. The bug was never really the
        missing djfbus, it was that the hardware's DEFAULT state sent an EXTREME value.
      
      - #9: order beats retry. The MIDI dance was a race symptom; gating on observable
        readiness (three independent checks, not a sleep) removed the race rather than
        automating the workaround. And idempotence is the property you want from anything
        you might run while panicking.
      
      - #15: a frozen Pulsar does not stop the audio — patterns live in ghci, not the
        renderer — which inverts the instinct to panic-kill the editor.
      
      - #19: du-deleted (436GB) vs df-freed (160GB) disagreed sharply; btrfs zstd + reflink
        sharing explains it, df is authoritative, don't chase the gap.
      
      - #27: border participates in layout and reflows text, outline does not — for any
        decoration toggling at performance rates, outline is the correct primitive.
      
      - #5: the HUD, pulsar-tidalcycles, and ~/.pulsar/styles.less are three separate
        version-control domains (the last untracked entirely, as user config). One visual
        bug can need coordinated edits across all three, which is exactly what #35 hit.
      PLN (Algolia) authored
    • docs(gig): mid-set recovery runbook + measured OPAL BPM arc · 9bdf3953
      Two gig-prep deliverables that needed no ears, written at J-10.
      
      RUNBOOK.md (#15) — the "when it breaks" playbook, organised symptom-first because
      that is how it actually gets read: you don't know the cause, you know the sound
      stopped. Ordered cheapest-first, with an explicit rule zero that rebooting
      SuperCollider is the LAST resort (30-120s of dead air and every pattern lost) and
      almost nothing needs it.
      
      The section worth having written down is 2b, the zombie control port. Observed live
      today: a leftover ghc process holds Tidal's port 6010, so a fresh Tidal boots, fails
      to bind, prints "Can't handshake with SuperCollider without control port" — and
      Pulsar looks completely normal. You eval and nothing happens, with NO error surfaced
      in the editor. That is the nastiest class of failure: silent, and indistinguishable
      from "my code is wrong". Detection is one line (ss -lunp | grep 6010, expect exactly
      one ghc and it should be the current one), so it's now in the pre-set checklist
      rather than only in the recovery section.
      
      Also captured: the mute-bomb rescue path (prelude.tidal as in-session hot-patch on a
      stale boot, with the tell that pulling fader 49 to 0 makes it WORSE not better);
      fader 77 being down as a silent global-gain failure that looks like nothing is
      broken; Pulsar being frozen NOT stopping audio, since patterns live in ghci and not
      the renderer, so Window: Reload keeps the music playing; and the LCXL USB-OUT
      endpoint stall where LEDs die but input survives (replug).
      
      Every procedure is labelled by how well it's actually established — §1a/2a/2b/2c/7
      have commands verified by running them today, §4/§6 come from documented prior
      incidents with known-good fixes but weren't re-tested tonight, and §5's gig-up.sh
      path was read from source rather than exercised end to end. A runbook you have never
      executed is a hypothesis, and it should say so.
      
      docs/2026-07-25-opal-bpm-arc.md (#12 input) — read setcps out of all 12 setlist
      tracks instead of trusting the backlog's [bpm] markers, and three disagree:
      - Gimme Acid: 80 measured vs 160 written — almost certainly the standard dnb
        half-time convention, but it decides where the track sits in the arc.
      - Sunshine: 144 measured vs 166 written. 22 BPM is far too big to be a rounding
        note, and it inverts the end of the set: the backlog plans Mafia(160) ->
        Sunshine(166) as a rising finish, but measured it is a 16 BPM DROP. Either the
        file drifted or the number was aspirational — needs a decision, since the shape
        of the finale depends on it.
      - Desire: 127 vs 129. Trivial, but it's the FINALE, so make them agree.
      
      Charted the measured arc and flagged three >40 BPM cliffs (->89, ->80, ->160). The
      89 and 80 ones look deliberate — a slow track between fast ones is a real dramatic
      move — and they are only a problem if the transition is unprepared, which is
      precisely what the riser audit (#33) and default-xfade (#13) exist to fix. Also
      noted the 124/124/128 plateau as the most transition-safe stretch in the set.
      
      Ordering itself is left as PLN's call; this is only the tempo dimension.
      PLN (Algolia) authored
    • docs(gig): riser audit correction — found the 12th track, and it's a THIRD idiom · 5a010b11
      Second pass on the audit. "Am i Doing it Right?" was reported as file-not-located;
      it exists as live/techno/do_it_right.tidal. No slug guess matched the title — it
      was identified by `setcps (89/60/4)` = 89 BPM against the backlog's [89*] marker.
      Recording that as a method: when title-to-slug guessing fails on this repo, the
      tempo line is the reliable identifier, not the filename.
      
      It changes the finding in a way that strengthens the conclusion rather than
      weakening it. The track DOES have a riser, so the count is 3/12 not 2/12 — but it
      is a third, mutually incompatible idiom:
      
        d10 $ mask "<f!4 t f!3>" $ loopAt 4 $ "risers:8" # gain 1 # cut 10
      
      An 8-cycle period firing on cycle 5, but loopAt 4 stretches the sample over 4
      cycles while the mask only opens for 1 — so what plays is a slice of a slowed
      riser, not a complete build. Could be deliberate (at 89 BPM a full 4-cycle build
      is very long) or a leftover; flagged for PLN either way.
      
      So the real headline is not the absence of risers, it's the inconsistency of the
      ones that exist. Three tracks, three idioms, disagreeing on all three axes:
      - phase: fires-last-8 (Take Five) vs fires-first-1 (Vague de CRIME) vs
        fires-cycle-5-of-8 (Do It Right);
      - gain: 0.8 vs unset vs 1 — the riser will not read consistently to the room
        across three tempos;
      - wrapping: Take Five AND Do It Right both lack gF/gM, so they are the only
        streams in the set the global filters and mutes cannot touch. Reach for gF1
        during either build and everything ducks except the riser.
      
      That third point is the one that could actually bite live, and it now affects two
      tracks rather than one. It also makes the gRiser proposal stronger: harmonizing is
      no longer "add the missing 9", it's "reconcile 3 disagreeing implementations and
      then add the missing 9", which is exactly the kind of thing worth doing once in a
      helper instead of nine times by hand at J-10.
      PLN (Algolia) authored
    • docs(gig): riser audit of the OPAL 2026 set — only 2 of 12 tracks have one · 1ae6f369
      Task #33 asked "does every OPAL track have a riser, and are they harmonized?".
      Answering it needed the setlist (backlog.md § OPAL 2026, 12 tracks with BPMs)
      resolved to actual .tidal files, then every d10 / riser / sweep / impact hit read
      in context. Result is worse than expected and worth having in writing before
      rehearsal rather than discovering it on stage.
      
      Findings:
      - Only 2 of 12 tracks have a riser: Take Five Drops and Vague de CRIME.
      - 5 tracks have d10 completely free (Bombe, WAP, Mafia, Sunshine, Desire) — a
        riser drops straight in.
      - 4 tracks have d10 already occupied by non-riser material (Piment's "Menace",
        Perfect's euclidian arps, Gimme Acid's "GIMME THAT", Revolution's sweet
        trigger) — those need a different stream, not a d10 paste.
      - Track 3, "Am i Doing it Right?", has NO .tidal file matching any slug I tried.
        Per the repo convention it must exist under a name I didn't guess. Flagged as
        an open question — the set is incomplete until it's found.
      
      The two existing risers disagree with each other, which is the actual #33 problem:
      Take Five sleeps 24 cycles and builds over the final 8 (a build-up INTO a drop),
      while Vague de CRIME fires on cycle 1 of 8 at the sample's natural length (an
      accent OUT of a downbeat). Same folder, opposite musical function — exactly the
      muscle-memory hazard when you're moving track to track mid-set. Also caught: Take
      Five's d10 is the only stream in the whole set with no gF/gM wrapper, so global
      filters and mutes cannot touch it — reach for gF1 during that build and everything
      ducks except the riser.
      
      Proposed one house helper, `gRiser period build`, derived verbatim from PLN's own
      working Take Five block rather than invented. loopAt is the load-bearing trick: it
      makes the riser's length independent of the sample's natural length, so one helper
      lands correctly from 120 to 166 BPM.
      
      Validated the proposal two ways before recommending it, per build-the-katana-first:
      - compiles clean against the real tidal-1.9.5 (ghc -fno-code, no stream started);
      - the generated mask is BIT-IDENTICAL to the hand-written "<f!24 t!8>" — queryArc
        over cycles 0-32 gives exactly 24 False then 8 True for both, compared
        elementwise, mine == theirs -> True. It's a faithful refactor, not a lookalike.
      
      Deliberately NOT committed to BootTidal.hs and NOT applied to any track: what the
      grep cannot answer is whether stock gain 0.8 / room 0.4 sits right under a 166 BPM
      liquid dnb track. That's ears. Suggested trial order is Desire (the FINALE, where a
      missing build costs most), then Mafia and Sunshine (the set's peak).
      
      Ties to #13: with default-xfade dN now baked in (98d073f2), every track launch
      already blends over 4 cycles. A riser is the musical counterpart to that structural
      crossfade — the xfade stops the seam being abrupt, the riser tells the room a
      change is coming. Together they're what turn "ctrl+enter and pray" into a transition.
      PLN (Algolia) authored
    • fix(boot): bake the live-validated prelude into BootTidal.hs — disarm the gF mute-bomb permanently · 98d073f2
      The problem: `gF1/gF2/gF3` in BootTidal.hs sent a value into `djfbus`, which is
      NOT a registered SuperDirt effect. The LCXL faders REST AT 0, so every gF-wrapped
      stream shipped an extreme value into a missing bus the moment it was launched —
      sound muted mid-set with no way back. Every mature ParVagues track wraps its
      streams in gF1/gF2/gF3, so this was armed on essentially the whole set.
      
      It was fixed and live-confirmed on 2026-07-25 ("f123 djfs seem to work great
      overall") — but only as a shadow-eval from live/lib/prelude.tidal. That fix lived
      in the running ghci and nowhere else: one SuperCollider reboot before OPAL and the
      footgun rearmed itself silently. J-10 is the wrong time to depend on remembering
      to eval a file.
      
      Approach — three blocks promoted from prelude.tidal into the boot file:
      
      1. SAFE-AT-REST global filters. Swap the phantom djfbus for CORE params that are
         always registered, mapped so fader-rest(0) == BYPASS:
           gHPF = range 20 8000   (rest ⇒ hpf 20, effectively open)
           gLPF = range 20000 180 (rest ⇒ lpf 20k, effectively open)
         gF3 is now literally `id` — a spare fader that cannot touch sound at all.
         Untouched ⇒ no effect, ever. Fader up ⇒ the filter sweeps in. Cannot mute you.
      
      2. DEFAULT-XFADE dN launches. d1-d12 become `xfade N . (|< orbit N-1)`. The orbit
         composition is the load-bearing part: a naive `xfade N` DROPS the orbit that dN
         bakes in, so transitions landed on the wrong LCXL column and the wrong Ardour
         stem. Added an orbit-correct `jumpTo N` for hard cuts. d13-d16 deliberately
         stay plain `p` — orbit 13 is the tidal-ears capture vsink and must not blend.
      
      3. gPanic — the ^93 gate the start_and_midi.scd panic-chord (73+74+91+92) flips.
      
      Validation: type-checked in a throwaway ghci against the real tidal-1.9.5 install.
      All signatures resolve as intended — gF1/gF2 :: Pattern ValueMap -> Pattern ValueMap,
      gF3 :: a -> a (proving the safe identity), d1 keeps its original
      Pattern ValueMap -> IO () shape so no track file needs an edit, d13 stays
      ControlPattern -> IO (), jumpTo :: ID -> Pattern ValueMap -> IO ().
      
      prelude.tidal is demoted from "source of truth we forgot to promote" to an
      explicit RESCUE SHEET: it now documents the stale-boot symptom (push fader 49,
      sound mutes instead of sweeping) and stays evaluable as an in-session hot-patch.
      One source of truth, one less thing to remember mid-set.
      PLN (Algolia) authored
    • feat(gig): panic-chord + LED feedback, always-on lean recorder, freebox archive complete · 29ce1039
      Three independent pieces landing together from tonight's live-debug/build session:
      
      1. Panic chord (task #34): holding LCXL push-buttons 73+74+91+92 together now
         flips a persistent silence toggle. These are momentary Note buttons (not
         latching CC), so a simultaneous hold is a natural chord — detected with an
         edge-triggered flip-flop in start_and_midi.scd (the only layer that sees raw
         per-button press/release state cleanly). Flips are echoed to Tidal as a new
         virtual control "^93", and to the LCXL's own LEDs via the Note-On method
         already validated in this project (reference_lcxl_led_stall memory). Tidal
         side is a plain gPanic gate in live/lib/prelude.tidal — PLN wires it into
         whichever track he's actively composing/testing, not applied globally.
         Not yet live-verified (needs a fresh boot) — tracked as task #34.
      
      2. tools/gig_record.sh: a lean, reusable, always-on stereo-master capture,
         answering "I want to always record all play, compressed." Auto-resolves
         whatever the current default PipeWire sink is at start time (so it follows
         the rig from onboard SOF SoundWire onto the UMC202HD arriving tomorrow with
         zero config changes) and encodes to Opus at 160kbps in hourly segments
         (~72MB/hr — 188GB free buys thousands of hours). start/stop/status/slice
         subcommands; slice does a fast stream-copy excerpt for quick auditioning or
         feeding into Slopmotion once that's available. Smoke-tested end to end
         (start, 8s live capture, ffprobe-verified valid opus, stop) before wiring
         into anything. NOT yet wired into gig-up.sh's auto-launch — PLN wants
         silent-by-default on every boot; that wiring is the next step.
      
      3. Local disk freed from 28GB (98%) to 188GB (81%) free. Root-caused why the
         session had crept there: the 436GB Ardour "Tidal Multi" session (deprecated
         in favor of the lean "Tidal Live" the gig now boots) was still sitting
         locally despite the freebox mirror already holding it byte-identical.
         Verified via fbk --dry-run (0 audiofiles needing transfer; the 1530-file,
         449,260,749,448-byte audiofiles tree matched exactly on both sides) before
         asking PLN to bless the delete — freed via `rm -rf` of audiofiles/export/
         peaks, keeping the tiny .ardour session file + midifiles as structural
         reference. (Real gain was 160GB not the naive 436GB sum — this machine's
         root is Btrfs with zstd compression, so du-reported sizes don't equal freed
         space 1:1; 188GB real free per df is still the number that matters.)
      
      Also: visuals/slop/README.md — documented the staged first test clip
      (Opal2024's 12_Mauerpark.flac, loudest 60s window at 2:53-3:53, chosen because
      it's the same track lineage as ete_a_mauerpark.tidal currently being edited)
      while the actual Slopmotion repo remains unlocated (org has 0 repos; asked
      PLN to get the real URL from Kevin/Shipow).
      PLN (Algolia) authored
    • feat(live): safe-at-rest gF filters + default-xfade dN prelude, and Slop-visuals scaffold · c71ff2d5
      #21 gF footgun fixed and #13 xfade confirmed live this session:
      - live/lib/prelude.tidal: nucleus of the ParVagues lib (#23). Two eval-blocks PLN
        can shadow into a running session (no reboot):
        * gF1/gF2/gF3 rebuilt on CORE hpf/lpf mapped so fader-at-rest (CC=0) == BYPASS.
          The old  (# djfbus N …)  referenced an UNregistered SuperDirt effect and rested
          at an extreme → muted the orbit with no recovery. New form structurally cannot
          mute: untouched = transparent, push fader = sweep in. PLN confirmed 'work great'.
        * d1..d12 = xfade N . (|< orbit N-1): every track LAUNCH crossfades 4 cycles AND
          keeps its orbit (old bare  xfade N  lost the orbit → wrong LCXL column/stem).
          Plus orbit-correct  jumpTo i  for hard cuts. PLN confirmed 'no more abrupt cut boom!'.
      - visuals/slop/: scaffold for Kevin's Slopmotion — README (NOW=generate shareable
        MP4s, LATER=gig visual engine) + FEEDBACK.md log to hand back to him.
      
      Not yet baked into BootTidal.hs — kept as evaluable prelude until fully road-tested.
      PLN (Algolia) authored
    • feat(gig): auto-launch is now perf-aware + preloads the set's samples · c35c9020
      Two live-play problems, both auto-handled by gig-up.sh so nothing lives in
      PLN's head at gig time:
      
      1. MIDI-burst CRACKS — root cause was NOT samples or RT priority (rtprio 95 is
         fine): the CPU was capped at max_perf_pct=55 (the quiet/cool desk regime),
         so a control sweep's CPU burst starved the audio thread -> xrun. gig-up now
         ASSERTS Standard (uncapped, governor performance) on launch via
         perf.set_mode('standard') — which also writes the desired-state file so the
         Bridge watcher holds it. Revert to Cool in the tray when done; skip with
         GIG_PERF=off. Confirmed live: toggling to Standard killed the cracks.
      
      2. LAZY-LOAD cracks — gen_preload() runs setlist_samples.py --last N --emit-sc
         into preload.scd before boot; start_and_midi.scd loads it after
         SCLOrkSynths.load, warming just the recent set's samples (last 15 tracks =
         55 folders vs the whole library). setlist_samples.py also gained bare-name
         track resolution (find under live/). preload.scd + gig-sclang.log gitignored.
      PLN (Algolia) authored
    • feat(gig): setlist_samples.py — preload just the set's samples (kill lazy-load cracks) · a4a76e03
      Live play cracked from lazy sample loading (~dirt.doNotReadYet=true): each
      sample reads from disk on first play mid-set → crackle + xrun spikes
      (764→1085 xruns during a track). Loading the whole library = slow boot + RAM;
      lazy = cracks. This tool finds the middle path: parse a set's .tidal tracks
      (or --last N by mtime), extract every token from every quoted string, and keep
      only those that resolve to a real sample folder (Dirt-Samples, extra/,
      tidal-drum-machines) — the library itself is the filter, robust against
      ParVagues' bare-string/# dialect that defeats s"..."-based parsing.
      
      --emit-sc prints a ~dirt.loadSoundFiles preload snippet. Validated: last 15
      tracks → 55 folders (vs hundreds in the library); bombe_dj → 8. Foundation for
      #26 (wire into boot) + #24 (soundcheck). t/f excluded (Tidal booleans).
      PLN (Algolia) authored
    • fix(gig): boot the real start_and_midi.scd, not stale main_fairbanks — root of… · c63dd3d3
      fix(gig): boot the real start_and_midi.scd, not stale main_fairbanks — root of silent synths & orbit errors
      
      gig-boot.scd chained main_fairbanks.scd (scratch: 8 output channels, 8 orbits)
      BEFORE start_and_midi.scd. main_fairbanks booted the server first, so PLN's
      real boot found it already up and its options were ignored — losing the 28
      channels, 14 orbits (d1..d12 + ears test), SCLOrkSynths.load, the MI global
      effects, StageLimiter + s.volume=2. Net live: d9-d12 silent, bassWarsaw/
      FMRhodes1 never loaded, orbit 8/10/11 errors spamming the post window.
      
      Fix (#18 simplification): launcher runs PLN's canonical sclang start_and_midi.scd
      directly; gig-boot.scd retired; main_fairbanks.scd kept as reference only.
      Apply = FULL sclang/scsynth restart (running scsynth keeps 8-ch options).
      PLN (Algolia) authored
    • checkpoint(tracks+gig): save-state before ParVagues-lib rationalization · e359a343
      Snapshot of the live-set material and gig docs before the planned lib
      restructure (dedup + a proper loaded ParVagues prelude so gF/gM/djf live in
      one place, not re-defined in scratchpads). Also protects secrets: .env +
      build noise now gitignored. Includes FOH_MASTER.md (master-bus hardening
      recipe) and the ordered-202HD shopping update. Track edits: desire,
      vague_de_crime, + new network_zero_un / bombe_dj / ca_fait_rien /
      electric_hammer.
      PLN (Algolia) authored
    • chore(gig): point gig-up at the lean 'Tidal Live' session · 2bc6a8e2
      Save-As-empty produced Tidal Live (no audiofiles — the 419G stems were NOT
      copied). Retargeted the launcher from the fat archival 'Tidal Multi' to the
      lean go-forward session. Tidal Multi stays put for the freebox stem archive
      (#19), then retires.
      PLN (Algolia) authored
    • feat(gig): make gig-up idempotent — converge the rig, never spawn dupes · 0c65fda8
      Follow-up to the live launch test: re-running gig-up while the rig was
      already up would have opened a SECOND Ardour on the same session (an
      'already open / locked' dialog) and a second Pulsar. Only SuperDirt was
      guarded (skip if scsynth up).
      
      Added ardour_up() (pgrep -f 'ardour-[0-9]', since the real binary is
      ardour-9.2.0 behind the ardour9 wrapper script) and pulsar_up(), and gated
      both launches on them. Now gig-up is a converge-to-ready button: run it at
      any point and it starts only what's missing. Verified live with the full
      rig up — SuperDirt/Ardour/Pulsar all correctly reported 'already running,
      skipping', nothing duplicated. Serves #18 (dance with sound, not setup).
      PLN (Algolia) authored
    • fix(gig): gig-up.sh crashed on launch — open_term read an unused $1 under set -u · e3043558
      First real end-to-end run of the ordered launcher aborted immediately at
      "starting SuperDirt": open_term() carried a vestigial `local cmd="$1"`
      (the sclang command is hardcoded inside the function), but the function is
      called with no arguments, so `set -u` turned the unused positional into a
      fatal 'unbound variable' before sclang ever spawned.
      
      Dropped the dead parameter. Re-ran: SuperDirt boots headless, claims the
      LaunchControl XL (aconnect 16:0 -> SuperCollider 128:3) BEFORE Ardour opens,
      then Ardour 9.2.0 (Tidal Multi) + Pulsar come up — the whole rig from one
      command, no disconnect/reconnect MIDI dance. Task #9 validated live.
      PLN (Algolia) authored
    • docs(gig): SHOPPING_LIST.md — stage-signal + go-bag purchases for OPAL 2026 · f1628d8b
      Per-item 2-line rationale + 3 example search queries, constrained to few-days Amazon
      or a Pigalle scavenge (deadline ~Aug 1). Priority = a USB interface with balanced line
      outs (the real fix for "you're too quiet": today FOH is fed from the laptop headphone
      jack). Then powered USB hub (LCXL LED stability), a DI-box fallback, a cable/adapter
      kit, and an optional passive monitor controller for the fine volume control the KRK
      wheel lacks. A sub is noted as post-gig — metering solves the bass blind spot for now.
      PLN (Algolia) authored
    • feat(gig): 1-click ordered launcher — SuperDirt-first, then Ardour, killing the MIDI dance · ba8e84a7
      Start-of-gig pain: whichever of SuperDirt or Ardour opens the LaunchControl XL first
      wins it, and when Ardour wins, sclang can't get the controller → the manual
      "disconnect MIDI / reconnect MIDI" dance mid-setup. The fix is ordering + a real
      readiness gate.
      
      gig-up.sh (repo root): brings SuperDirt up FIRST and WAITS until it has actually
      claimed the controller, then launches Ardour + Pulsar. Readiness = three unambiguous
      signals, all of which appear during main_fairbanks.scd's boot: scsynth alive +
      UDP :57120 listening (SuperDirt.start) + a "SuperCollider" ALSA-seq client present
      (MIDIClient.init has run). 120s timeout (sample loads can be slow) → proceeds with a
      clear warning rather than hanging. Best-effort throughout: terminal auto-detected
      (konsole/kitty/…, background-log fallback), Ardour/Pulsar binaries probed by name,
      skips SuperDirt if scsynth is already up.
      
      gig-boot.scd: one eval that runs the real boot (main_fairbanks.scd) + the LaunchControl
      CC->OSC bridge (start_and_midi.scd) so the rig comes up fully armed in a single file;
      loaded by relative path to stay portable.
      
      Validated offline: bash -n clean; the three probes correctly read 'down' with SuperDirt
      dead; ardour9 + pulsar resolve; the Tidal Multi session path exists. End-to-end (the
      live MIDI gate) to be confirmed on the next real boot.
      PLN (Algolia) authored
    • fix(audio): shield SuperDirt from the display stack — headless sclang (QT_QPA_PLATFORM=offscreen) · d8346672
      Root-caused the 2026-07-25 overnight-sleep crash where the music (and Ardour)
      died on resume. It was NOT an audio-hardware failure — it was the *display stack*
      taking down the sound.
      
      Evidence (all at resume, ~11:17):
        11:17:31  kwin_wayland (pid 2603) SIGABRT — the Wayland compositor aborted on
                  resume-from-suspend (deep internal assertion; core shows abort→libc).
                  Plasma respawned the whole session (new kwin/plasmashell/Xwayland,
                  confirmed by their ~1h40m uptime vs the machine's 1d+).
        11:17:35  ardour9: "Fatal IO error 104 (Connection reset by peer) on X server
                  :1" — Ardour, an X11 client on Xwayland, died with the session.
        11:17:35→44  scsynth SIGABRT via std::terminate — ~4s after kwin, in the
                  cascade: sclang is a Qt6/Wayland client, so it lost its display
                  connection when kwin died and crashed, and scsynth (which it hosts)
                  aborted on the broken IPC.
      
      Why short naps survive but an overnight one didn't: a quick suspend/resume just
      pauses the Wayland connection; the long/overnight cycle triggered a GPU/DRM reset
      that made kwin actually crash — and a crashed compositor severs every client at
      once.
      
      Fix: sclang doesn't need a display. QT_QPA_PLATFORM=offscreen runs its Qt without
      connecting to any display server, so a compositor crash/restart can no longer reach
      it — the SuperDirt brain becomes immune to the entire Wayland/X lifecycle. SuperDirt
      uses no GUI; the interpreter and post window still work in the terminal. Validated:
      `QT_QPA_PLATFORM=offscreen sclang` compiles the class library and reaches the
      SuperCollider 3.14.1 banner with zero wayland/qpa errors.
      
      collide.sh is the only launcher (perf.sh only reprioritizes a *running* sclang, it
      doesn't spawn one), so this single line closes the gap.
      
      Not addressed here (harder, separate levers): kwin's resume-crash itself is a
      Plasma/GPU-driver issue — the standing mitigation is running dGPU-off
      (./gpu-mode.sh integrated), which removes the Xwayland-on-dGPU DRM reset that's the
      likely abort trigger and would also spare Ardour's :1 connection.
      PLN (Algolia) authored
  5. 24 Jul, 2026 3 commits
    • feat(perf): gpu-mode switch — the real dGPU-off lever + cockpit surfacing · 44263086
      The last unclaimed thermal lever. Our 2026-07-24 finding: on this Plasma-Wayland
      box Xwayland pins the RTX 2060 'active' regardless of runtime-PM (control=auto,
      --rtd3), so the dGPU never actually suspends live and keeps drawing ~4-5 W + idle
      heat. The ONLY real off is EnvyControl's 'integrated' mode (blacklist nvidia) — but
      that's a persistent config change that needs a re-login, categorically unlike the
      instant perf.sh modes (governor/EPP/RAPL). So it gets its own tool, not a perf-mode.
      
      gpu-mode.sh (new, at repo root beside perf.sh)
      ----------------------------------------------
      A guarded EnvyControl front-end: `query` (default — manager mode + live dGPU
      runtime state, with the right advice per mode), `integrated` (dGPU off; warns you
      lose CUDA/NVENC for demucs/audio-ML until you switch back), `hybrid` (restore with
      aggressive --rtd3 3 = D3cold-when-idle). Every switch confirms (skip with -y), runs
      `sudo envycontrol ...` (prompts for your password), then prints the exact re-login
      step (reboot safest; verify with `./gpu-mode.sh query` + `nvidia-smi`).
      
      Deliberately NOT added to the passwordless perf-audio sudoers whitelist: this is a
      rare, considered, re-login-required switch — the web Bridge and tray must never be
      able to power-cycle the GPU config unattended. Runs by hand only.
      
      Cockpit surfacing (Bridge + tray)
      ---------------------------------
      - perf.py: gpu_manager_mode() reads `envycontrol --query`, cached 300s (the mode is
        static within a login session; a re-login restarts this --user service and
        refreshes the cache), defensive → None if envycontrol absent. Added to snapshot()
        as `gpu_manager`.
      - Bridge UI: the `gpu` chip folds it in — shows `off↻` when integrated-but-pending-
        re-login (dot green: this is the quiet goal), and in hybrid its title now names the
        manager mode and points at `./gpu-mode.sh integrated` for a real off.
      - Tray tooltip mirrors it (`gpu awake·hybrid`, or `off↻` when armed).
      
      Validation: 24 perf tests pass (2 new — parse+cache, and absent→None; snapshot-shape
      now asserts dgpu/gpu_manager/thermald). Live: `./gpu-mode.sh query` reads
      hybrid + active(0000:01:00.0) and fires the Xwayland warning; Bridge /api/perf serves
      gpu_manager=hybrid; both services restarted clean.
      PLN (Algolia) authored
    • docs(perf): 2026-07-24 live-playback highlighter marker profile · f7898292
      The playing counterpart to the July-18 idle capture: mid-set highlight-stats
      shows 580 markers live vs 16 active, accumulating ~40/min (matches July's
      34/min idle) — the position-marker pool still never evicts, so the 07-19
      "marker leak fixed" is incomplete and the idle 82% renderer burn (the
      break-time heat) shares this exact root. UI-smoothness only; audio untouched.
      PLN (Algolia) authored
    • feat(perf): smart-silent mode + Bridge/tray thermal cockpit · 2960c9fb
      Problem: the laptop ran hot even idle, and the bafd3133 RAPL watt caps were
      committed but NEVER deployed — the live /usr/local/sbin/perf-audio predated
      them, so every --cool set clock-% while RAPL sat at the BIOS 135W (measured
      90C under an all-core encode). Watts are the thermal lever, not percent.
      
      - perf.sh: new --silent ("smart-silent") — powersave + EPP power + 55% clock
        ceiling + a hard 12W/25W RAPL cap + allow_dgpu_suspend (nudge the discrete
        GPU toward D3cold) + audio still RT. Quieter than --cool without disabling
        turbo (low PL2 keeps SuperDirt DSP transients from xrunning). Tunable via
        SILENT_PL1_W / SILENT_MAX_PCT.
      - perf-audio.sudoers: whitelist --silent (flag-scoped, no wildcard).
      - tools/bridge/perf.py: MODES gains silent (tray + web inherit it);
        detect_mode disambiguates silent vs cool by EPP; snapshot() gains cores_pct
        (per-cpu util deltas), dgpu (runtime_status), thermald, watts (live RAPL
        caps), power_w (actual draw — null until energy_uj is unlocked, then lights).
      - ui/index.html: the bar is a cockpit now — 16 "dancing cores" (height=util,
        colour=temp, hover=cpu/util/temp), folded chips (watt cap / dGPU asleep-awake
        / thermald), a distinct teal+snowflake tint for Silent, and a 60s temp+power
        sparkline.
      - perf-tray.py: the icon's decorative sine is now a REAL temp-history
        sparkline; tooltip + menu fold in cap/gpu/thermald.
      
      Verified: POST /api/perf {silent} -> live RAPL 12/25W; {cool} -> 28/50W; the
      135W furnace is leashed. perf tests 7-pass.
      PLN (Algolia) authored
  6. 23 Jul, 2026 1 commit
    • fix(bridge): detect resume via CLOCK_BOOTTIME, not MONOTONIC · 3a973eec
      Field-caught by the watcher itself. The laptop suspended ~10:11 and resumed
      11:21:46; resume reset the pstate cap (a brief 4.4 GHz clock spike in the monitor
      log), and the watcher DID restore cool — but at 11:22:04 via the ~30s periodic
      drift check ("[perf-watch] drift: reasserted cool"), not the fast 2s resume path.
      
      Cause: the resume detector used time.monotonic(), and CLOCK_MONOTONIC does not
      advance while the system is suspended — so on thaw the thread saw only its normal
      ~5s sleep, gap << threshold, resume missed. The periodic safety net (belt-and-
      suspenders) is what actually caught it.
      
      Fix: measure the gap with CLOCK_BOOTTIME, which counts suspended time, so a resume
      now shows as a large gap and triggers the immediate force-reassert (2s settle).
      Falls back to monotonic if CLOCK_BOOTTIME is unavailable. Verified present on this
      box (uptime read OK); watcher restarts clean with on_ac=True desired=cool.
      PLN (Algolia) authored