1. 05 Sep, 2026 30 commits
    • fix(midi): cut the surface's direct leg to SuperCollider, every 2s · 361a05b9
      SuperDirt's boot runs MIDIIn.connectAll (start_and_midi.scd:19), so SC
      subscribes to every hardware MIDI input it can see -- the LCXL included.
      The surface then fed SC twice: once raw, once through Midi Through. And
      MIDIFunc.cc takes `src` and never checks it (start_and_midi.scd:51), so
      every CC fired the handler twice and every button noteOn double-fired
      into ~lcxlChordCheck. A double noteOn on a toggle is a no-op: that is how
      a mute button silently stops working.
      
      lcxl3-driver's prune() handled this for the v3 board, but only while it
      runs -- and it was stopped today (stale binding to an unplugged mk3,
      Conflicts=-killing the mk1 painter). A hand-run `aconnect -d` fixed it
      live and then SC restarted and re-armed it, which is the whole argument
      for putting it in the reconcile loop that already owns the wiring.
      
      Rule 3 matches the RAW board on type=kernel, never on name alone:
      lcxl3-driver publishes a virtual port called 'ParVagues LCXL3' that
      matches the same name candidates, and cutting the driver's translated
      feed would kill the path we actually want. Only surface->SuperCollider
      links are cut; -> Midi Through and the aseqdump taps that gig-log and the
      Pulsar HUD read all survive, verified live.
      
      Tested by arming the bug (aconnect 20:0 130:3, 20:1 130:4) and watching
      the loop cut both and settle: lcxl-path.py went from "LCXL -> SC direct"
      back to "LCXL -> Midi Through -> SC", logged once, no per-tick spam, and
      gig-log kept counting touched controls.
      
      Also logs PLN's ear-feedback on the classic-LCXL play-test (rose_rouge
      works great; midiviz landed) to the archivist's notes.
      PLN (Algolia) authored
    • docs(board): correct my own count — 14 stale uploads, not 1 · 84e37fec
      The first version of this amendment said one OPAL track on SoundCloud was
      stale. Then I read the account instead of reasoning about it, and it is
      fourteen: the entire Aug-12 first cut is up, thirteen of it under junk
      permalinks (/opal26-wap, /12-mafia-12, /ceci-nest-pas-une-bombe) that roll
      call could never have found, because roll call only looks where it expects a
      track to be. Same error the board made before me, one size smaller.
      
      The finding that explains PLN's ear exactly: eight of the fourteen stale
      uploads carry cover art and not one of the six correct v4 tracks does. The
      wrong cut is the one that looks finished.
      
      Also corrected: 'every other track matches to the second' was never a verdict,
      it was a tolerance. SoundCloud reproduces full_duration to the MILLISECOND, so
      at the ±1.5s a transcode seems to deserve, the Aug-12 and v4 cuts of /wap
      (361.0 vs 361.6) and /perfect (315.0 vs 314.6) are the same track. That is how
      six stale uploads passed a duration check in August.
      
      And two coincidences caught before they could become deletions: /saria-demo
      (2025) and /au-revoir-lord-toyota (2020) match OPAL to 50ms by chance on a
      132-track account. Duration is a strong discriminator and still not provenance.
      
          the ones with the cover art were the wrong ones.
          six months of a set played once, and the tell
          was never in the sound -- it was in which
          of the twenty ghosts looked ready.
      PLN (Algolia) authored
    • docs(claude): the canonical gig-metadata path lost its next/ segment · 111c903e
      CLAUDE.md pointed at ../../Web/www/next/content/lives/, which has not existed
      since www hoisted the app out of next/ in 6e51fff. The correct path is
      ../../Web/www/content/lives/{year}/{slug}.md.
      
      This is a worse bug than a typo in docs. The instruction it sits inside is
      'Mastering != metadata. Never invent gig metadata... Verify there and cite the
      path — if a fact isn't in the canonical source, ask.' A reader who follows the
      documented path gets a false 'not found' from the one source they were told to
      trust, and the honest next step from there — asking, or worse, filling the gap
      from an intermediate script — is exactly the failure the rule was written to
      prevent. A stale read is how a Write becomes a delete.
      
      The tide-table scripts already had it right (build_catalog_view.py's LIVES
      carries a comment naming the hoist commit); only the instruction was behind.
      PLN (Algolia) authored
    • chore: project config and the logo source · eefd7066
      - process-palette.json wires the editor's command palette to the rig's own
        entrypoints (Init MIDI Channels -> init_midi.sh, and the rest). The whole
        'one entrypoint, fewer files' goal is only real if the editor knows where
        the entrypoints are, so this is rig config, not editor preference.
      - sketch.properties points the Processing sketch at its main .pde.
      - Logo.xcf is the editable source of the ParVagues mark, from 2025-05. The
        armada design system is documented down to its type stack and its reserved
        brand magenta, and the one file you cannot regenerate from any of that is
        the layered original. 1.2 MB to never have to redraw it.
      
      Deliberately still untracked: TODO.md. It is a paused 2026-06-22 perf/thermal
      board, and TASKS_DUMP.md has been the board since 2026-08-16. Committing it
      would put a second answer to 'what is next' in the tree, which is the shape
      that keeps biting us. PLN's call: fold its two live items into TASKS_DUMP and
      delete it, or keep it as a scratch file outside git.
      PLN (Algolia) authored
    • docs(notes): archive today's two ear-signals before they evaporate · ec9c7d4b
      Written earlier today by the gear-repair session and left uncommitted, which
      for this particular file is the one failure mode that matters: performance_notes
      IS the archive, so an uncommitted entry is not a note, it is a memory.
      
      Two signals, both about the rig rather than the audio:
      
      - *"ok just played rose_rouge works great!"* — first confirmation that the
        CLASSIC LCXL (mk1, 1235:0061) is a fully viable performing surface once
        lcxl-leds-watch owns the paint. Worth recording precisely because the rig had
        drifted into treating the mk3 as the only real board; the classic is a
        fallback, not a downgrade.
      - *"and the midi mon is beautiful!"* — on first sight of tools/bridge/midiviz.py.
        A taste signal about the rig's own UI, and the follow-up ask (always-open,
        always-on-top, every desktop) says it plainly: midiviz is an instrument of
        the rig, not a debug tool.
      
      CLAUDE.md asks for every reaction captured durably because this corpus feeds
      sampling, effect choices and the auto-mastering heuristics. Durably means
      committed.
      PLN (Algolia) authored
    • feat(tools+tracks): the last loose sources come aboard · f5d34615
      Four kinds of orphan, all real source, none of it generated:
      
      - tools/sweep_state.py — 'the surface needs a sweep', as a fact the rig can
        act on. Companion to the LCXL3 driver thread.
      - armada/tide-table/kick_bass.py — the deterministic kick-vs-bass tiebreaker,
        the one split CLAP cannot do reliably because both live low. Exactly the
        'validate by analysis, never infer role from the name' rule in code.
      - armada/tide-table/calibration.stem.json — joins calibration.json and
        calibration.whole-mix.json, which were already tracked. A calibration set
        missing one of its three members is a trap for whoever reads the other two.
      - install_superdirt.sclang — pins SuperDirt v1.7.2 and recompiles. The boot
        path is exactly where 'simple, non-brittle, one entrypoint' matters.
      - live/collab/benoit/tous_ensemble.tidal and live/midi/nova/nujazz/
        nouvelle_couleur.tidal — two scores. nouvelle_couleur carries a '-- url:'
        header naming its backdrop source, per the scene standard.
      - visuals/{maze,nightly_repairs,there,there0}.js — four hydra sketches.
      
      Python entrypoints staged 100755 via --chmod, not left to the working tree.
      
          the drawer is empty now.
          everything in it was something,
          which is why it took three weeks to look.
      PLN (Algolia) authored
    • feat(mix): the mix/ toolbox lands — ten scripts the index never knew · a14aee74
      Ten shell tools plus FORMAT.md and MIX.md, written 2026-07-29 and untracked
      since: stem-stats, energy-profile, onset-detect, silence-detect,
      transition-detect, mix-meta, mix-render, mix-normalize, mix-master, lufs-check,
      and lib/progress.sh. FORMAT.md defines a <project>.mix.json metadata contract
      whose tools 'read existing metadata and augment (don't overwrite unless told)'.
      
      Committed with the exec bit set IN THE INDEX, which is the part that bites:
      3fdf5b97 had to restore it on 63 scripts, and MIX.md invokes every one of these
      as ./stem-stats. A file that is executable in the working tree and 100644 in
      git is executable exactly until someone else clones it. Note also that
      'git commit -- path' silently drops a staged mode flip (the trap 1667bb02
      documented), so this went in without a pathspec after proving nothing else
      was staged.
      
      mix/take94's 833 MB stayed out (a5a5b37). The scripts are the artifact; the
      take is the input.
      
      ️ Flagged, not resolved: this overlaps ../tidal-ears' mastering CLI, which is
      the canonical recipe per MASTERING.md. Two toolboxes that both master a set is
      the shape feedback_one_parser_per_concept warns about. I am not merging them
      blind — committing first so the overlap is visible in one tree instead of
      half-invisible in an untracked directory. PLN's call which one is the spine.
      
          ten small knives in a drawer nobody opened,
          sharp the whole time.
      PLN (Algolia) authored
    • docs(log): land three captain's logs that were written and never committed · 71516e25
      Written 2026-08-14, uncommitted for three weeks: the -inf fader forensics on
      Tidal Live.ardour, the corpus triage that found '51 broken tracks are actually
      22 — the harness was the biggest bug', and the 192 kHz master-format audit
      ('probe, never assume'). All three are marked shareable: true, which is the
      whole point — these logs are the source material for the posts and videos,
      and they were sitting one bad rm away from not existing.
      
      The rule in armada/tasks/README.md is 'write the entry when the task lands,
      while it's fresh'. It worked: the writing happened same-day. It was the
      committing that didn't, because git status had become unreadable (see a5a5b37).
      Documentation discipline is not one habit, it is two, and the second one is
      the one that survives a hard drive.
      
      Left for PLN, one line: all three carry 'log: 025' in their frontmatter, which
      025-fifteen-intros-are-not-a-set.md already owns. They also use dated filenames
      like 2026-08-03-j5-audio-dropout.md rather than the README's NNN-slug.md.
      Renumbering is a call about his own archive's spine, not mine — and the dates
      are 2026-08-14, which lands them BEFORE 039, so the next free numbers would
      read out of order.
      
          the log was kept. the log was not carried.
          a ship's record that never leaves the desk
          is a memory, not a record.
      PLN (Algolia) authored
    • chore(git): fence the bulk so the sources can finally land · 039ff575
      Three weeks of untracked working tree, and the reason nothing was committed is
      that 'untracked' had stopped meaning anything: 957 MB of take audio and scene
      mp4s sat in the same status output as three finished captain's logs, so the
      whole list read as noise and got skipped as one.
      
      Sorted by what each thing actually is, against the precedent already in this
      file and in the index: mix/'s ten scripts are source and go in, mix/take94's
      833 MB does not. The .js scene sketches are source, the 124 MB of rendered
      mp4 is not. calibration.json and calibration.whole-mix.json are tracked, so
      calibration.stem.json belongs with them; bounds_*.json is generated output
      whose builder is already tracked, so it does not.
      
      No new policy here — just applying the one written at the top of this file to
      a pile that had grown too mixed to read.
      
          the tide went out three weeks ago
          and left everything on the sand together:
          charts, driftwood, a gigabyte of water.
          sorting is not cleaning. sorting is deciding what was ever yours.
      PLN (Algolia) authored
    • feat(tide-table): sc_generation_lens — which cut is actually on the account · fe8ec16c
      PLN opened the day hearing it: 'our private cuts of OPAL are badly cut
      begin/end unaligned'. The board had one theory (a 24s truncation) and it was
      wrong. This lens found the real shape: the ENTIRE Aug-12 first cut is still
      on SoundCloud — all 15 tracks — living beside the 6 correct v4 ones under a
      litter of ad-hoc permalinks (/opal26-wap, /12-mafia-12, /ceci-nest-pas-une-
      bombe), and eight of the stale ones are the ones wearing artwork. So the
      wrong cut is the one that looks finished. Of course he heard it.
      
      Duration is the discriminator, and it is far sharper than assumed: measured
      on this account, SoundCloud's full_duration reproduces the source to the
      millisecond (477.600 uploaded, 477.600 reported). Tolerance is 50ms, not the
      seconds you would allow for a transcode — a loose tolerance here buys no
      safety, it only invents ambiguity between cuts that differ plainly.
      
      Three things I got wrong first, each left in as a guard:
        - filenames are not authoritative. 'Perfect <3' uploads to /perfect-3, so a
          filename-derived slug reported two live tracks as NOT UPLOADED. Titles
          come from the plan now, which is what the uploader actually used.
        - streaming and club masters share every duration, so offering both as
          rival candidates marked all 14 AMBIGUOUS. Identical generations collapse.
        - a duration collision is not provenance. /au-revoir-lord-toyota (2020) and
          /saria-demo (2025) both matched OPAL to 50ms. Anything uploaded before
          the oldest render on disk cannot be a cut of it, and a coincidence must
          never reach a delete manifest — so the date floor is a hard gate and the
          discards are printed, not silently dropped.
      
      The second pass is the one that earned its keep. Roll call only looks where
      it expects a track to be, so it can never see a stale cut hiding under a
      different slug. Strays scans the whole account. Absence needs proof.
      
          fifteen ghosts of a set played once,
          each wearing the cover art of the real thing --
          the lens does not listen, it only counts seconds,
          and seconds were enough.
      PLN (Algolia) authored
    • fix(catalog): rebuild view+generated catalogs, stop as_of freeze · f1fa4847
      catalog.generated.json was stale since 2026-06-23 (73 tracks / 23 gigs) and
      missing opal-festival-2026 + cosmicfest-2026 entirely — it never got rebuilt
      after catalog_view.json picked those gigs up on 2026-09-02. Re-ran the
      pipeline (build_catalog_view.py then build_catalog.py):
      
        catalog_view.json:      81 tracks / 25 gigs / 63 takes (unchanged — was
                                 already current, only as_of + minor EDA-coverage
                                 deltas moved)
        catalog.generated.json: 73→81 tracks, 23→25 gigs, now carries both missing
                                 2026 gigs, both validate clean against models.Catalog,
                                 and track-id sets match viewgenerated exactly
                                 (no silent drops either direction)
      
      Root cause of the as_of staleness: build_catalog_view.py hardcoded
      `AS_OF = "2026-06-06"` as a literal instead of stamping the actual build
      date, so every regeneration since kept reporting the same frozen date no
      matter when it ran. Now computed as `date.today().isoformat()`. Checked the
      other suspected bug (a next/content/lives hardcoded path) — not present;
      LIVES already points at .../www/content/lives without next/ since 6e51fff.
      PLN (Algolia) authored
    • docs(board): the OPAL SoundCloud cuts are the Aug-12 generation, not v4 · d81816a6
      PLN opened the session hearing badly-cut begin/end on the private OPAL
      uploads. Measured all three render generations against what the board
      recorded on the account: /take-five-drops sits at 6:57 = 417 s, which is
      exactly the Aug-12 first cut — four days and two boundary revisions before
      the ear-verified v4. The complaint had a mechanical cause.
      
      Ruled out before believing anything: tracks_v4_streaming matches
      segments_v4.json to the millisecond on all 14 (CRIME's 2s rest and
      REVOLUTION's reverb tail included), the upload plan's titles/files/durations
      are aligned and exact, and the ear file's 19:33 mtime is the signoff record
      being written, not a later boundary edit. Nothing needs re-cutting; the
      approved audio is already on disk. This is a re-upload problem.
      
      Two things stand in the way and one of them is PLN's call: SCW.delete() is
      hard-gated to the test namespace so we cannot remove our own stale uploads,
      and SoundCloud skips rather than replaces an existing permalink. Also: no
      sc_state.json was ever written, so the account is the only record of what
      landed — sc pull --artwork recovers ids, permalinks, durations AND the covers
      PLN asked to save, read-only, in one pass.
      
      Plus the catalog answer he asked for (generated.json is 2 gigs stale behind
      the view, 14 gigs uncatalogued), www's OPAL tracks.json still on segments_v3
      where Slopmotion reads it, the CLAUDE.md next/ path that no longer exists,
      and two stale-opens git says already shipped.
      PLN (Algolia) authored
    • feat(tracks): veridis_quo jam tweaks + rose_rouge notes from the validated set · e1b75e28
      veridis_quo (PLN, first jam): hats ply gate moved to ^43 (E3, its own column),
      bass line up an octave. rose_rouge: comments from the night it re-validated the
      rebuilt rig — a sample-kit idea and the bassline's new name.
      PLN (Algolia) authored
    • fix(lcxl3): OLED home repaint — dedup on what was SENT, reassert every 20s · d3f58c6e
      The footer froze at '?REC 00:10' on the glass during the first real take. No
      code-level freeze reproduced; what WAS wrong: the ghost mirror recomputed state
      instead of mirroring device writes (so diagnosis lied), and the 2s tick wrote
      unconditionally with no dedup. Now: _home_cache records the string actually
      sent, the tick writes only on change (repaint ≤2s, revert ≤2s after stale), and
      the 20s label-reassert forces a full repaint so dedup can never mask a device-
      side reset. Hardware verdict awaits the next recorded take.
      PLN (Algolia) authored
    • fix(gig-log): three lessons from the first real take · 7e64736b
      Bug 1: Ardour flushes stems in ~12MB bursts every ~3s — a 2s stop-grace read
      every gap as stop/start and reset the clock eleven times in one 20s take.
      Grace is now 6s, with the measured cadence in the comment.
      Bug 2: transport-button feedback IS bit-gated — /set_surface feedback 8→9
      (button-status + heartbeat); the 'not bit-gated' reading died on the live rig.
      Bug 3: a mute lens is not a dissenting lens — combine_rec only counts OSC when
      arm AND roll are concrete, so 'connected but silent' can no longer stamp
      conflict:true on ground truth. src labeling now names actual contributors.
      Regression tests drive RecPass through the real flush cadence; 169 green.
      PLN (Algolia) authored
    • feat(lcxl3): the OLED footer becomes a REC clock while Ardour records · d1947eef
      Third line of the home screen: 'ParVagues' at rest; 'REC mm:ss' while the
      gig-log k:'rec' stream says audio is landing on disk (tail-read of the newest
      gig jsonl, same style as last_eval); '?REC' when the files and OSC lenses
      disagree — a timer the performer must not trust blindly is marked, not hidden.
      Stale or absent signal degrades to the plain footer, never crashes a redraw.
      Pure decision function rec_line() + tests.
      PLN (Algolia) authored
    • fix(gig-log): resolve the Ardour session without argv — a 3-rung evidence ladder · abad7515
      A GUI-launched Ardour has a BARE argv (only gig-up passes the session as an
      argument), so argv-matching alone left the files lens blind on the real rig.
      Resolution now climbs: argv → /proc/<pid>/cwd holding a *.ardour → open-fd
      symlink targets (most-fds wins on ambiguity, logged once). Injectable proc_root
      makes each rung testable against a fake /proc; 9 new tests reproduce the exact
      live gap (163 green). check-mix.py shares the latent argv-only bug — flagged in
      the sync comment, not fixed this pass.
      PLN (Algolia) authored
    • feat(gig-log): k:'rec' — two lenses on Ardour actually recording · 33897540
      'Recording X had track Y playing at time Z' becomes answerable. Files lens =
      ground truth (stems growing in interchange/, stat-only, 2s grace, RecPass state
      machine); OSC lens = Ardour's own word (/set_surface feedback=8 handshake to
      :3819, listens :3820, /rec_enable_toggle + transport — the manual's real name
      for master arm, not /rec_enabled). Records on transitions + every tick while
      recording; lens disagreement ships as conflict:true, never smoothed over.
      44 new tests (155 total green), stdlib-only, --no-rec escape hatch.
      PLN (Algolia) authored
    • fix(tools): parvagues-gear ships executable · 619bbae5
      The straggler from 3fdf5b97's sweep: with core.fileMode=false a PATHSPEC commit
      reads modes from the working tree (where they're invisible) and skips a staged
      mode-only flip — index-only commits are how mode fixes land here.
      PLN (Algolia) authored
    • chore(rig): regenerate parvagues-rig.target from the inventory · 1667bb02
      lcxl-leds-watch (manual policy) leaves the target; lcxl3-driver and perf-tray
      join it. Generated by rig_units.py --target — the ~/.config copy is a symlink
      into the repo, so the regen IS this diff.
      PLN (Algolia) authored
    • perf-tray + gig-up: default-on 'Cut GPU at gig launch' (kill TuParles, verify… · 90afa239
      perf-tray + gig-up: default-on 'Cut GPU at gig launch' (kill TuParles, verify dGPU RTD3, name holders if it won't sleep)
      PLN (Algolia) authored
    • fix(lcxl3): lap 1 starts at first track load, not process construction · da1c7c73
      The OLED's track clock resets 'a new track is a new lap' — but skipped the
      FIRST load, so lap 1 inherited the constructor's zero. Invisible while the
      driver restarted with every session; under the persistent unit (log 039) it
      counted the idle hours before the first load: 24:27 on a fresh rose_rouge.
      PLN (Algolia) authored
    • feat(rig): the LCXL3 driver joins the supervised rig — one inventory, one painter · 96f13223
      The night's lesson, converged: the driver only ran when a session remembered to
      start it, then the session's background task got OOM-reaped and the surface sat
      untranslated with every check green.
      
      - tools/lcxl3-driver.service: repo-owned unit like its siblings; retry-forever
        hotplug posture; Conflicts=lcxl-leds-watch so exactly one painter owns the
        board; follow-mode (no --track) so paint tracks the loaded file.
      - rig_units.py: lcxl3-driver + perf-tray rows (THE list is complete again),
        new 'manual' boot policy so --ensure can never start the v2 painter into a
        Conflicts= kill; --list --boot filter for shell consumers.
      - parvagues-gear: moved into the repo; derives its sets from rig_units --list
        instead of being the fourth hardcoded copy; autostart-on now defers to
        --ensure (it used to enable parvagues-sc at login against policy).
      - gig-up.sh leds(): detects the plugged surface generation and hands the LCXL3
        to the driver unit; v2 path invoked via python3 (exec-bit-proof).
      PLN (Algolia) authored
    • fix(tools): restore the exec bit on 63 scripts, in the INDEX where it counts · 3fdf5b97
      core.fileMode=false means chmod fixes your tree and nothing else — the 203/EXEC
      class (log 032) recurs on every fresh checkout because the index said 644.
      git update-index --chmod=+x is the repo-level fix; every shebang'd script under
      tools/ + gig-up.sh + perf.sh now ships executable.
      PLN (Algolia) authored
    • feat(tools): gig-preflight — assert the rig is ready BEFORE you play · 774087a0
      The rig is heavily instrumented and nothing ever looked. gig-log has been
      sampling freq, per-core cpu, throttle deltas, fan rpm, per-process cpu/RSS and
      xruns at 1 Hz for weeks (512k samples in the current file), yet every failure
      this session was discovered by feel, mid-session, at the worst moment. This is
      the looking.
      
      Every check exists because it silently cost a real session on 2026-09-04/05:
      
        * Composed an hour at 800 MHz because the Bridge's perf-watch reasserted
          `silent` every 30 s, undoing each fix within half a minute. Cost 2671
          xruns/hour. Every throttle counter read zero — with HWP, epp=power makes the
          CPU *choose* its floor, so the failure has no error message.
            -> check_perf, check_regime_agreement
        * `gluck` refused to load for 90 minutes. The symlink was perfect; sclang had
          been up 6 days and loadSoundFiles scans folder NAMES once, at startup, so a
          bank added later is invisible. Pulsar's "restart SuperDirt" button is a no-op
          here (SuperDirt is a systemd service, not Pulsar's child) and honestly
          reported 142 h uptime while looking fine.
            -> check_superdirt_fresh
        * Samples/crutch was a symlink pointing at ITSELF since Oct 2025. Dead bank,
          no complaint from anything.
            -> check_sample_integrity
        * A new LCXL3 showed default LEDs because the resolver name-matches the mk2
          product string, while the watcher logged "-> 27 controls" and looked healthy.
            -> check_midi_surface
      
      Three design rules, all learned the hard way:
      
        1. Green is not evidence. `active`, exit 0 and silence all lied during this
           session, so every check reads the LIVE value from sysfs/procfs/the device
           and prints the number it saw. No check trusts a status.
        2. Every FAIL carries its fix as a runnable command. A preflight that reports
           a problem without the remedy just adds a step between you and playing.
        3. It never mutates the rig. Read-only by construction — a preflight that
           writes is one more un-arbitrated writer, the exact bug class it detects.
      
      16 checks. First run found 1 fail + 3 warns, including a genuine xrun rate of
      2224/hour with every CPU line green — a problem we had not noticed and which the
      clock fix does not explain.
      
      Known over-sensitivity: check_xruns uses the raw total, and two of the top three
      sources (input.hydra_in, the built-in Speaker sink) may not be in the live
      monitoring path at all. Xruns on an idle sink inflate the count without being
      audible; it should weight by nodes actually in the path.
      
        gig-preflight.py            human report, exit 1 on any FAIL
        gig-preflight.py --quiet    only WARN/FAIL, for gig-up.sh
        gig-preflight.py --json     for the tray/Bridge
      PLN (Algolia) authored
    • fix(lcxl): find the mk3 — "LCXL3 1" is not "Launch Control XL" · f7d38407
      A new Launch Control XL 3 sat there showing factory-default LEDs. Not autostart
      (lcxl-leds-watch was active), not wiring (aconnect showed the surface connected
      to Midi Through): the port resolver matches the product name "Launch Control XL",
      and the mk3 enumerates as "LCXL3 1". So _find_hw_port()/_find_seq_port() both
      returned None and every frame went into the void — while the parse stage kept
      logging "-> 27 controls" and looking perfectly healthy. A failure with no error
      message, which is the worst kind and the whole reason gig-preflight now exists.
      
      NAME_RE also matches \blcxl\d*\b. The inline HUI exclusion becomes SKIP_PORT_RE,
      which additionally drops the mk3's DAW and "To DIN" ports — the mk2 exposed one
      non-LED port, the mk3 exposes three, and sending a frame to any of them is a
      silent no-op.
      
      Validated: resolver now returns hw:1,0,0 and seq 20:0; it returned None before.
      
      NOT fixed here — the LED dialect. The device answered a Universal Device Inquiry
      with:
      
        F0 7E 00 06 02  00 20 29  48 01  00 00  01 01 0B 39  F7
                        Novation  family        firmware 1.1.11
      
      and per Novation's programmer's reference the mk3 lights controls in RGB, one
      message per control:
      
        F0 00 20 29 02 15 01 53 <control index> <R> <G> <B> F7
      
      versus this file's mk2 frame: device id 0x11, command 0x78, a User-1 template,
      batched (index, value) pairs, and a bicolor 2-bit red x 2-bit green = 16-state
      colour model. So mk3 support is a second dialect plus a replacement colour model
      (RGB doesn't map onto 16 bicolor states), and the per-control message form means
      a full 40-control repaint costs 40 SysEx messages where the mk2 cost one — the
      paint loop will need rate limiting. Tracked in SRE TODO.d.
      PLN (Algolia) authored
    • fix(perf): Pulsar stays fast in every mode — the editor IS the instrument · e42586b8
      perf-audio deprioritized Pulsar to nice 10 in cool/extreme and nice 5 in
      standard, on the theory that the editor should yield to audio. That trade buys
      nothing. The entire audio chain runs SCHED_FIFO *above* it — scsynth 90,
      sclang 85, PipeWire 95, WirePlumber 95, Ardour 80 — and a realtime policy
      preempts every CFS nice level unconditionally. So Pulsar at -5 cannot take a
      single cycle from SuperDirt; it only wins against ordinary processes (browsers,
      indexers, builds), which is exactly what we want during a composing session.
      
      Now nice -5 in every mode, overridable with PULSAR_NICE=NN (0 = neutral,
      positive = the old behaviour). -5 rather than 0 because the renderer is a single
      JS thread and its work is bursty: it has to win the CPU on the tick after a
      keystroke, then yield. No root needed — @audio's `nice -19` grant in
      /etc/security/limits.conf is what makes the same renice work for pln.
      
      Validated: installed to /usr/local/sbin/perf-audio, re-applied via
      `bridge.py perf standard`, all Pulsar processes confirmed at nice -5 live.
      PLN (Algolia) authored
  2. 04 Sep, 2026 1 commit
  3. 02 Sep, 2026 9 commits
    • merge: tempo_timeline, the confirmed gig facts, and the board · 6850da97
      Two commits, 4 files, reviewed as a diff against master.
      
        tempo_timeline.py       tempo per window in a rendered track, to test whether
                                a planned track was actually played. Answered WAP: not
                                played (median 124.5 over segment 1, nothing at 133
                                above the confidence floor), calibrated first on a
                                declared-160 track which returned 161.5.
        gigmeta sidecar         PLN's confirmed set time (~20:30), his verbatim
                                approval of "Le Jardin", and the WAP question moved
                                from open to resolved with both sources named.
        build_gig_tracksjson    resolve the frontmatter-only fields before writing
                                _provenance, so their origin is recorded whether or
                                not --md-out was passed.
        TASKS_DUMP.md           the board after the release session: six resume
                                points, two of which are the kind that look done when
                                they are not.
      
      Two bugs of mine are in the branch rather than tidied out of it: the octave fold
      was an infinite loop for any value a sub-octave band cannot represent, and the
      per-frame FFT loop was ~100k Python-level calls for a five-minute stem.
      PLN (Algolia) authored
    • docs(board): CosmicSet is on the shelf — the board after the release session · b3279c8d
      The two-set release board from 2026-09-01 is half discharged. CosmicFest-26 went
      from "finished on disk with nowhere to go" to a private 15-item release behind a
      canonical www gig page; OPAL-26's remaining upload work is now the only release
      thread left, and it is unblocked rather than in flight.
      
      Six resume points, each with the exact command and the wanted output, because
      two of them are the kind that look done when they are not: the uploader saying
      "N uploaded" is not evidence that N tracks exist (two were accepted, verified,
      and then vanished), and renaming the mix's permalink without regenerating the
      plan would make the next run re-upload 693 MB rather than skip it.
      
      Also recorded: the www branch is committed but unpushed because that repo is on
      github and no key was loaded, the CosmicFest plan still carries `_UNSIGNED` so
      nothing may be flipped public, and the two levers left on the record are the LOW
      end (the real harmony lever, much bigger than the air shelf) and the deferred
      club variant. Plus a note that piment_bresilien.tidal is PLN's own edit.
      PLN (Algolia) authored
    • feat(tide-table): tempo_timeline — check whether a planned track was played · 3ed1bc2d
      PLN's set plan lists WAP [133] second at CosmicFest and the recording has no WAP
      in it. He remembered dropping it — "didnt play WAP i think in the end [...]
      direct from Bombe DJ to Something about Drums, its what i chose that day" — and
      asked me to check rather than take his word, which is the right instinct: the
      rival explanation is that WAP IS in the recording, inside segment 1, and the
      boundary pass read two tracks as one. Duration cannot separate those (track 1 is
      6.8 min, an ordinary length here for one track and also for two short ones).
      
      Tempo can, because the two candidates differ: Bombe DJ is 124 and WAP is 133,
      which is 0.484 s against 0.451 s per beat — 7% apart, well inside what an
      inter-onset histogram resolves. So this walks a rendered track in windows and
      reports what tempo each window is actually playing, on the demucs DRUMS stem
      rather than the master, because the question is about kick timing and a stem is
      far cleaner to find onsets in than a stage-mic mix.
      
      Method deliberately copied from tools/tempo-lens.py, which measures a live orbit
      and cannot read a file: spectral-flux onsets, then the MODE of the inter-onset
      intervals — never the mean, which over a pattern with ghost notes lands between
      two real values and reports a tempo nothing is playing.
      
      Calibrated before it was trusted, on There's Something About Drums (declared
      160): median 161.5. Then the answer, on segment 1 with --expect 124 --alt 133:
      median 124.5 over 26 windows with a steady pulse, and NO window at 133 above the
      steadiness floor. One 20 s window reads 132.5 at confidence 0.11, below the
      floor and with no neighbour agreeing — WAP as a played track would occupy twelve
      to twenty-four consecutive windows, not one. So: not played. PLN's memory and
      the audio agree, and the boundary pass did not miss a track.
      
      Two bugs of my own on the way here, both worth the record:
      
        * the octave fold was an INFINITE LOOP. Folding into a band of [100, 180) by
          doubling-and-halving never terminates for a value like 99: 99 -> 198 (out
          the top) -> 99 -> 198, because no power of two lands it inside a band
          narrower than an octave. Two processes sat at 98% CPU while I assumed the
          machine was merely busy with the upload. The fold is now closed-form
          (`bpm * 2**-floor(log2(bpm/lo))`) into [X, 2X), and the CLI takes
          --octave-from because an octave is the only honest width: tempo from audio
          is determined up to a factor of two and no further.
        * the per-frame FFT loop was ~100k Python-level calls for a five-minute stem.
          One strided view and one batched rfft instead.
      
      Also, since the meta sidecar is where facts live: PLN's confirmed set time
      (~20:30), his verbatim approval of "Le Jardin", and the WAP question moved from
      open to resolved with both sources named. And build_gig_tracksjson now resolves
      the frontmatter-only fields (time, location, CTA, tags) BEFORE writing
      `_provenance`, so their origin is recorded in tracks.json whether or not
      --md-out was passed. tracks.json is the file that outlives the invocation.
      
      Side finding for PLN's ears, reported rather than acted on: over the last ~80 s
      of segment 1 the drums stem reads 161.5 BPM — the tempo of the NEXT track. Two
      readings, and I cannot choose between them from here. Either the segue is a long
      livecoded blend with Something About Drums layered in early (which is what
      "direct" looks like in TidalCycles), or the boundary at 6:46.6 sits later than
      the music does. His ear owns that one.
      PLN (Algolia) authored
    • docs(log 038): part III — the record reaches the shelf, and three notes I had already written · 89f9d712
      PLN asked three questions this morning and each one landed on a different
      failure of mine, so the log gets a part III rather than a tidy epilogue.
      
      The demucs OOM he spotted was a fossil: the killed first attempt (8.2 GB, 2 of
      14 sections) sits in the journal forever beside the rerun that succeeded at
      7.6 GB. A unit's history is not its state — verify the artefacts.
      
      The +5-vs-+8 recommendation is corrected. I had called +8 "the conservative end"
      against a generic genre norm; against OPAL v4, the master he actually signed off,
      CosmicFest is already 6.6-8.1x brighter above 4 kHz on a third of the sub. So
      +5, and the lesson is about naming the reference: a genre norm and the artist's
      own shelf can point opposite ways, and only one of them answers "does it sit with
      my other release". Recorded with the caveat that most of that table is the
      recording, not the master — and that the unspent lever for harmony is the LOW end,
      not the shelf we A/B'd all night.
      
      And the gig metadata was never missing. His own repo held the date, the location,
      the stage and the lineup; his backlog held the five movements. The blocker was
      that I had not looked, and in that gap I had written an unsourced 2026-08-23 into
      the spec. Wrong by a day, with the answer two files away.
      
      Plus the three things that only running the pipeline could show: POST /tracks is
      dead AND burns the S3 upload first, --force never worked for the case it was
      named after, and the catalog gap was a cycle rather than a stale cache.
      PLN (Algolia) authored
    • merge: the CosmicFest release pipeline, and a shelf that cannot collide · b747cc70
      PLN: "lets merge on main all these tooling improvements and go on".
      
      17 commits, 16 files, all reviewed as a diff against master before merging and
      not just at the tip — the lesson from the 200-file merge in ea52a32c..2f947c2c.
      Twelve are new files, four are edits: build_release_plan.py, catalog_view.json
      (a regeneration, 73 -> 81 tracks, dropping none), the CosmicFest spec, and
      TASKS_DUMP.md.
      
      What it adds, in one line each:
      
        master_stemless.py     a bus-only master for a gig with no stems
        demucs_sections.py     separation in bounded chunks, sections and global
        eda_stems.py           the required stem EDA, from separations instead
        setlist_to_segments.py an ear-called setlist becomes segments
        compare_masters.py     an A/B in numbers, so the ear spends itself on taste
        build_gig_tracksjson.py  a gig page generated from sourced facts
        check_permalinks.py    the shelf is one namespace; refuse a collision
        finish_cosmic.sh       everything downstream of the masters, unattended
      
      Delivered: two masters in spec (-14.0 LUFS, -0.9 dBTP, 44.1/24, duration exact
      to a microsecond against the source), a 14-track split that verifies clean, 14
      section stem sets plus a global pass, and three EDA questions answered — the
      missing air is cymbals, the kick is present 44.8% of the set against OPAL's
      59%, and the global separation pass is a measured waste of time (corr
      0.943-0.977, level deltas under a quarter of a dB).
      
      Four things I got wrong are recorded in the branch rather than tidied out of it,
      because three shared one shape worth naming: a check whose failure looks exactly
      like success. A dropped rtk grep plus a block-buffered log made a live render
      look dead, and two processes wrote the same paths for forty minutes. A speed
      claim counted section directories, which appear when a writer OPENS. An edit
      no-op'd while printing that it had worked. The fourth was not new — the first
      full render came out at 192 kHz, a loudnorm trap this project had already
      documented and I did not read before writing a new chain.
      
      Not merged and still PLN's: the +5 vs +8 air-shelf call, and a release_signoff.
      The CosmicSet upload on the shelf right now is PRIVATE and its plan says
      _UNSIGNED.
      PLN (Algolia) authored
    • fix(release): the shelf is one namespace and a live catalogue reuses titles · 34b02925
      Caught before --go, on the last check I nearly skipped. The CosmicSet plan
      resolved four of its fifteen permalinks onto tracks Opal had already uploaded:
      /you-my-sunshine, /take-five-drops, /piment-bresilien, /eh-ouais-je-funk. Of
      course it did — those are the same scores, played at both gigs a fortnight
      apart, and a permalink is derived from the title.
      
      What makes this worth a gate rather than a fix is the failure mode. SoundCloud
      is idempotent by permalink and the uploader treats a taken permalink as "already
      done" — correct for resuming an interrupted 1.4 GB run, catastrophic for a
      different recording of the same track. So the upload would NOT have errored. It
      would have reported success with four tracks missing from the release, or, with
      --overwrite, replaced Opal's audio with CosmicFest's. Neither is visible from
      the terminal that ran it. `feedback_absence_needs_proof`: a release that is
      quietly short looks exactly like a release that is complete.
      
      check_permalinks.py resolves a plan against the live account and sorts every
      permalink into FREE / already-up-as-this-same-audio / COLLISION, using duration
      as the discriminator because it is the one property that survives SoundCloud's
      transcode and is already in the plan. A collision exits 1. Verified both ways:
      it fails the un-suffixed plan naming all four real collisions, and passes the
      suffixed one 15/15 free. Its shelf query carries access=playable,preview,blocked
      because scform.py's own notes record that private tracks are omitted without it
      — and an audition release is private by definition, so the gate would have
      called every permalink free.
      
      build_release_plan.py gains --permalink-suffix to fix the cause, so a gig's
      permalinks are scoped to the gig and no future pair of sets can collide.
      
      Two things about --force, which this release is the first to actually need (the
      signoff cannot precede the upload when SoundCloud is where PLN listens):
      
        * it never worked. The no-signoff branch did `return problems` before reaching
          the override, so the flag documented as "emit even without a valid ear
          signoff" could only override a STALE signoff, never a missing one. The early
          return is gone and both problems now reach the same override.
        * it now demands --why, and records the reason, the timestamp and a
          do-not-flip-public warning in the plan's own `_UNSIGNED` block. A terminal
          warning scrolls away; the JSON is what the uploader reads. An override with
          no stated reason is indistinguishable from an approved release two weeks
          later, and the gate exists precisely so nobody has to remember.
      
      Left alone deliberately: the plan cannot tell private from public — the uploader's
      --sharing flag decides that — so _UNSIGNED documents rather than enforces.
      PLN (Algolia) authored
    • feat(tide-table): a gig page can be generated without inventing anything · c5aaf892
      The CosmicSet release was finished on disk and could not move, because every
      upload adapter goes through build_release_plan.py and that refuses to run
      without a canonical www gig page. Writing the page by hand was never an option —
      `feedback_metadata_vs_mastering` exists because a previous release copied a wrong
      ALBUM string out of an intermediate script. So the generator learned to take
      supplied facts instead.
      
      --meta takes a JSON sidecar where every field is {value, source, locator, date}.
      The builder reads the values, carries the provenance into `_provenance` in the
      output, and prints "nothing left to invent" ONLY when no non-derivable field is
      still empty. A bare value is accepted — PLN often just says a thing in chat —
      but it is recorded as unsourced, so the gap stays visible instead of becoming
      indistinguishable from a researched fact.
      
      Which mattered immediately, because ONE OF MY OWN STRINGS WAS WRONG. The gig
      spec said 2026-08-23. I had written that with no source. This machine held the
      answer in two places all along: www/PRODUCT.md:21 says "cosmicfest_v2.67Hz
      (20-23 August 2026, jour J Saturday 22)", and the invite's own lineup section is
      headed "le 22 août, dans le jardin". PLN confirmed: "yea it was samedi 22
      indeed". The spec is corrected and now says where the date comes from. That is
      the second unsourced string of mine to survive into a spec, hence a sidecar with
      a locator column rather than one more hand-filled field.
      
      The set's five movements are not invented either: Ouverture / SUNSET / We call it
      NuJazz / NUIT / FINALE come from PLN's own plan in backlog.md, which
      `reference_gig_tracklist_sources` names as canonical. The sidecar records the one
      discrepancy rather than smoothing it: the plan plays WAP second and the recording
      has no WAP, so every section from track 2 on shifts up by one. Track 1 is 6.8
      min, in line with other single tracks (3 is 6.6, 7 is 6.3), so it does not look
      like two merged — but that is an inference and it is labelled as one.
      
      Sample coverage went 7 empty lists -> 0, and the cause was not staleness. The
      first two tracks I chased (Rose Rouge, LiveCode Parade) were absent from
      catalog_view even after a rebuild, because catalog_view's track universe comes
      from `load_gigs()` globbing the ALREADY-PUBLISHED content/lives/**/tracks.json.
      A track's first gig page can therefore never find itself in the view: the file
      that would add it is the one being written. That is a cycle, not a stale cache,
      and rebuilding forever would not have fixed it. The builder now falls back to the
      same `tidal_score.orbit_sounds` the view itself calls — one parser, reused, so a
      new track's sample list cannot disagree with a catalogued one. With the page in
      place the cycle closes: the rebuilt view goes 73 -> 81 tracks, dropping none, and
      both tracks are catalogued.
      
      Also: "collab" joins STYLE_DIRS, because OPAL already published it that way for
      every live/collab/* score. style_of takes the last matching path part, so
      live/collab/nova/techno/x still reads techno.
      PLN (Algolia) authored
    • fix(finish): the global-stem gate compares against the master now, not a magic number · 696ba12a
      A threshold of 3700 seconds against an expected 3779 accepted exactly the state
      the pass sits in for minutes: it writes in 120 s chunks, so 31 chunks is 3720 s —
      one short — and "basically done" sails through a loose comparison. My own monitor
      fired a premature COMPLETE on that very number, which is how I noticed: the check
      and the bug had the same blind spot because I wrote both.
      
      It now reads the expected duration from the master itself and allows two seconds,
      so the check cannot drift from the thing it checks.
      
      The fallback message says what a short global file would actually cost, which is
      the part worth naming: it does not FAIL the comparison, because compare_global
      takes min(len(a), len(b)). It compares fewer seconds per track and reports thinner
      evidence under identical column headings. Silent degradation in a summary nobody
      watches being written is worse than an error.
      
      Second attempt at this commit. The first edit silently no-op'd — a scripted
      replacement whose search string carried escaped line-continuations that did not
      match the file, while the script printed "gate tightened" unconditionally, and I
      reported success on the strength of that print. Caught by grepping the file. This
      version asserts the anchor exists before replacing and asserts the content changed
      after, then proves it by grep rather than by message. That is the third instance
      tonight of the same failure shape — a check whose failure mode is indistinguishable
      from success — so the assertions are the actual fix, not the gate.
      
      Held until now deliberately: finish_cosmic.sh was mid-execution, and bash reads
      scripts incrementally, so editing a running script can corrupt the run.
      PLN (Algolia) authored
    • docs(log 038): the results — two masters, 14 tracks, and a clean negative on the global pass · 5885da5f
      Everything landed. Two full masters differing only in air shelf, both at -14.0 LUFS
      / -0.9 dBTP / LRA 7.5-7.6 / 44100 Hz 24-bit, and 14 split tracks verifying ALL OK.
      Duration is 3779.317551 s against a source of 3779.317550 — the load-bearing number,
      because it means every one of PLN's playhead calls stays valid and the split lands
      exactly where he put the cuts rather than approximately there.
      
      Q1: the missing air is CYMBALS. Drums hold 0.367% of their own energy above 8 kHz
      against 0.084% for the runner-up, a 4.4x concentration where evenly-spread energy
      would have meant codec hiss. So opening the shelf is licensed by measurement, and
      how far is not — hence an A/B rather than a number I liked. The framing that
      matters: both candidates are still BELOW typical brightness for electronic masters
      (8-16 kHz at 0.40% and 0.63% against a 1-3% norm), so the choice is "quite dark vs
      slightly less dark", not "safe vs aggressive". The A/B is also confound-free:
      identical loudness, identical true peak, range within 0.1 LU, only the tilt differs.
      
      Q2: the floor is worse here than OPAL. Kick active time averages 44.8% against the
      59% project_floor_problem measured for OPAL-26 from real orbit stems, worst at
      #2 22.8%, #3 33.4%, #8 36.9%. A stage mic in a room explains it on its own, so this
      is a lead rather than a verdict — but it is the first time this gig's floor has been
      measurable at all.
      
      Q3, PLN's own question, and the answer is a clean negative: the global pass was not
      worth running. Correlations 0.943-0.977 against the per-section stems with level
      deltas of at most 0.21 dB. The two separations are the same stems. The prediction
      held exactly — htdemucs' 7.8 s receptive field means a global pass cannot give the
      model more musical context, so only normalisation differs, and it barely moved. It
      cost ~2.5 h of CPU competing with the master render to establish a quarter of a dB.
      Sections only next time; the question was worth asking once, not twice.
      
      Side finding: bass is the least stably separated of the four sources (0.9432,
      lowest), consistent with the same run putting PunkAChien's bass into `other` — bass
      stem at -81.0 dBFS while `other` was the loudest stem in the set.
      
      The log also records three self-inflicted errors that share one shape: checks whose
      failure mode was indistinguishable from success. A dropped rtk grep plus a
      block-buffered logfile that made a working job look dead, so two renders raced on
      the same paths for forty minutes. A speed claim built on counting directories that
      appear when writers open. And a scripted edit that silently no-op'd while printing
      success unconditionally. Plus the 192 kHz master, which was not a new bug at all —
      topic_postprod_mastering has carried that trap since tidal-ears hit it, and I wrote
      a new mastering chain without re-reading the omnibus that exists to prevent it.
      PLN (Algolia) authored