- 28 Aug, 2026 5 commits
-
-
The v3 and the 169 tracks disagree about what a CC means, and only one of them is cheap to change. Paint is DAW-mode-only. Tested rather than believed, in three steps with PLN watching the surface: magenta on the MIDI port in standalone did nothing; the same paint on the DAW port with DAW mode off did nothing; `daw on` followed by the same bytes lit all 24 rings and 16 buttons. So the guide's "only available once DAW mode is enabled" is a hardware fact, not an artefact of how the document is organised. But DAW mode's CC map is fixed, and it overlaps v2's with DIFFERENT meanings. The map was read off the hardware, one control per row, not off the PDF's low-resolution diagram: fader1 -> cc5 ch16, A1 -> 13, B1 -> 21, C1 -> 29, E1 -> 37 ch1, F1 -> 45 ch1, shift -> 63 ch7. row v2 (lcxl_grid) v3 DAW A 13-20 13-20 identical B 29-36 21-28 C 49-56 29-36 <- v2's row B numbers D 77-84 5-12 E 41-44, 57-60 37-44 F 73-76, 89-92 45-52 Sixteen indices collide. The one that decides it: v3 #41 is button E5, while v2 #41 is button E1 — the kick gate the whole #94 migration just aligned 169 files onto. Renumbering would move the kick gate four buttons to the right, silently, with nothing erroring. That is the same failure class b5ad8b6c was written to end, so the corpus keeps its numbering and software absorbs the difference. Three things make this cheap rather than clever: SuperCollider's binding is `MIDIFunc.cc({...})` with no cc, channel or src filter — only the number matters, so a pure renumber is sufficient and no channel juggling is needed. Every encoder row is still in ABSOLUTE mode (queried: rows 1/2/3 all read 0), so a v3 encoder already behaves like a v2 pot. Relative mode needs an integrator that owns the value — genuinely better, since it kills pot-pickup outright, and it is where paint-by-value belongs — but it is a separate change and `--relative` is left as an experiment that honestly warns the map no longer applies. The v2 numbers are DERIVED from lcxl_grid.ROW_CCS positionally rather than retyped, so if the authored table moves, the translation follows. Publishing a virtual port rather than resolving SuperCollider's once: SC runs MIDIIn.connectAll a single time at boot, so a port that appears later is never connected, and a binding resolved once is the rig's most repeated failure mode. The driver re-asserts the aconnect link on a timer, the same shape as the two reconciler units. midi-autoconnect had the same stale-name bug: it hardcoded "Launch Control XL", which matches neither v3 port, and since it routes aconnect's complaints to /dev/null the failure was completely silent — Ardour stopped hearing the surface with every check green. It now resolves the source, preferring the driver's translated port over the raw device, and echoes what it picked once per change rather than every two seconds.PLN (Algolia) authored -
4d9f1059 rescued these two scripts from $HOME, where they had been living un-versioned, and committed them without the exec bit. The symlinks in ~/.local/bin resolved fine to a file systemd then refused to run: Process: ExecStart=/home/pln/.local/bin/tidal-ardour-autoroute.sh (code=exited, status=203/EXEC) Active: activating (auto-restart) An auto-restarting unit reads as busy rather than broken, which is why rig_units reported 'activating' and nobody chased it. Net effect: the autoroute that puts twelve orbits onto Ardour's Tidal NN tracks has not run since 23 Aug — the exact regression 4d9f1059 was written to end. The recurrence has a cause, and it is repo config: core.fileMode = false here, so git ignores exec-bit changes from the working tree entirely. That is why 4e0d63ae already had to fix the mirror image of this (+x on disk, 644 in the index), and why chmod can never be the whole fix in THIS repo — it silently does nothing as far as the index is concerned. `git update-index --chmod=+x` is the tool that works. Both halves done: chmod for this checkout, update-index for the index, so a fresh clone is born runnable.
PLN (Algolia) authored -
Not an extension of lcxl-leds.py, deliberately. v2 and v3 are different protocols, not different constants: v2 is a template/palette SysEx driving 3-colour buttons, v3 has true RGB per control, a 128x64 OLED, endless encoders, and a CC map that only exists in DAW mode. Folding v3 in would produce a file that lies about both. When the numbering settles, lcxl_grid.py stays the one authored table and this becomes its transport. Every byte sequence in here is quoted from the official programmer's reference v1.0, not recalled — RGB is 01h 53h <idx> <R> <G> <B>, the bitmap is 09h with 1216 bytes and a 7Fh terminator rather than F7h, and the feature CCs live on channel 7 of the DAW in port. Transport is python-rtmidi via mido rather than shelling out. aseqsend on this system takes -s as a FILENAME: `aseqsend -p 20:0 -s "B0 25 05"` prints 'cannot open B0 25 05' and exits 0. A silent no-op, which is how a first paint attempt can look like a hardware answer. lcxl-leds.py shells out that way today, which is worth revisiting. The subcommand that matters is `keystone`. The guide files 'Colouring the surface' and 'Controlling the screen' inside the DAW mode chapter, prefaced 'only available once DAW mode is enabled'. If that is how the document is organised rather than a hardware restriction, paint works in standalone and the corpus keeps its v2 numbering — a 169-file difference. So it gets tested in three steps with PLN's eyes on the surface, not assumed either way. `sweep` exists for the same reason: the DAW-mode index map is a low-resolution diagram in the PDF, so we light each index in turn and read the map off the hardware.
PLN (Algolia) authored -
PLN said snare was 'also a default tidalcycles CC0/public domain pack'. Upstream disagrees: tidalcycles/Dirt-Samples tracks 52 files under sn/ and zero under snare/. The 90-file snare bank is local and was unidentified. His second memory was the one that paid — 'the very early tidalcycles docs pointed me to that sample pack'. The banks come from slab.org/tmp/samples-extra.zip, the Tidal Club course pack from Week 1 lesson 2. Proven by reading the zip's central directory over a range request, so 113 MB stayed on the server: SNARE0, SNARE119, KICK142, CLAP163 are all in it, and the local folder is named samples-extra. Three candidates died on dates and names, which is worth recording so nobody re-runs them: Blood Sport prefixes its banks bs- and holds no SNARE*.wav; the 90s-sample-cds torrent was downloaded 2024-12-20, after these files' 2019-11-07 mtime; MF_Drums_Samples names its files BD_MF_Valve11.wav. And there is no content-based route at all — AcoustID needs song-length audio, not 0.4s hits. The uncomfortable part: this makes the ledger worse, not better. The pack ships no licence for these banks — the only licence file in it is Blood Sport's own. And snare's files carry ISFT 'Sonic Foundry Sound Forge 6.0', IENG 'TJ', ICRD 2003-08-30, i.e. 24-bit one-shots rendered on a 2002 commercial DAW. An official course pack establishes provenance, not licence. So kick, cleared on 2026-08-16 on the note 'TidalCycles official / samples-extra CC0 pack', rests on the same unverified premise. Same pack, same absent licence, but cleared is releasable and unknown blocks. One question to yaxu settles six banks at once.
PLN (Algolia) authored -
The board carried 'can third-party SysEx paint arbitrary text to the OLED?' as UNVERIFIED, with a note that it mattered: HUD v2's alarms are derived state, not control values, so a closed OLED meant the alarms stayed on screen. The official programmer's reference settles it — arbitrary 128x64 bitmaps, and the firmware ACKs each frame specifically so you can animate. Two more close the same way. LED brightness is CC 111 and non-volatile, so open taste call #11 stops being an argument about whether a bypassed filter should be bright and becomes a knob. And endless encoders emit deltas, so pot-pickup does not exist to solve. The new one is worse than the ones it closed. DAW mode is the only mode the guide documents paint in, and DAW mode's CC map is fixed and PARTLY OVERLAPS v2's with different meanings: v2's row-B knobs 29-36 are v3's row 3, and v2's faders 77-84 are v3's relative-encoder row 1. Nothing errors. Controls just move one row. That is the exact failure class b5ad8b6c spent 169 files fixing, so that branch does not merge until this is settled. Also retracted, with evidence: DIN is not our answer to the USB-hub single point of failure. aconnect shows only Midi Through and the controller — this machine has no MIDI DIN input at all, so DAW mode disabling the DIN outputs costs nothing today. And snare is still not cleared. 'Also a default tidalcycles CC0 pack' is a name collision: upstream tracks 52 files under sn/ and zero under snare/. The 90-file snare bank is local and unidentified, and still gates 4 tracks. CosmicFest is closed as a mastering input by PLN's call. Five probes agree nothing was captured to this machine; he had a stage mic and rates it poor.
PLN (Algolia) authored
-
- 23 Aug, 2026 4 commits
-
-
PLN: "so many tracks still having ^42 instead of ^41 for kicks its confusing." Remap phase 2 (5910aacc) column-aligned the board so column N IS orbit N, but it was scoped to the FOURTEEN files of the OPAL set. Everything else still spoke the layout from when gMask held CC 41, where an orbit's gate row started one column late: d1 -> ^42, d2 -> ^43. button-column-audit found 169 such files, every one of them shift +1 — a single layout generation, not a scatter of typos. Layout-shifted files: 169 -> 0. Buttons on their own column: 35.7% -> 78.6%. TWO SYSTEMIC FIXES IN migrate-columns.py, both found by running it on the whole corpus instead of a setlist. Each refused a batch rather than writing bad Haskell, which is the only reason they were cheap to find: 1. THE SEGMENT BOUNDARY WAS DEPTH-BLIND. A chain step ends at the next line starting with `$`/`#`, but a `$` inside an open paren is a CONTINUATION, not a new step. So a multi-line body truncated after one line, left depth 1, and the balance check refused the file. 10 of 169 were unmigratable. The boundary is now paren-aware, and the hit line is no longer assumed to BE a step start: `^NN` is routinely referenced from the middle of a body, so we walk back to the nearest enclosing step whose segment both balances and contains the hit. Negative depth was the tell that the anchor was too late, not that the source was broken. 2. COMMENTING OUT A `$`-STEP CAN UNDER-APPLY THE STEP ABOVE IT. Not a paren problem — an ARITY problem, and silent-eval is what caught it: 4 files stopped compiling while 165 were fine. $ midiOff "^57" (mask "<[t f]!0 ...>") <- a function, still hungry $ stack [ ... ] <- its argument Comment the `stack` and GHC says "midiOff is applied to too few arguments" and the whole do-block dies, all twelve orbits — far worse than the cross-orbit trigger the comment-out was avoiding. `#`-steps stay commentable (`#` takes a ControlPattern, so a shorter chain is still well-typed); `$`-steps are now ANNOTATED and left live, and the FIXME says so, because an annotated line IS still firing. VERIFIED against a pre-migration baseline, not against hope: * non-compiling tracks: 17 before, 17 after, and the SETS ARE IDENTICAL — zero newly broken, zero newly fixed. All 17 predate this work. * silent orbits: the same 19, in the same tracks, only line numbers moved. * pvlint: 30 -> 26 errors, no new error signature. Warnings 996 -> 1041: the +45 are the annotated-but-live `$`-steps honestly reporting that they still reach another column. Errors down, honesty up. * fix-button-roles: 3 clean role rewrites applied, then 0 remaining. The role report is otherwise unchanged from HEAD, so the migration introduced no gate/gesture inversion — the class only PLN's ear catches. * migrate-columns --plan re-run: 0 moves. Converged. Left for a second pass, deliberately: 118 sites where an orbit's two own-column buttons carry the same role (e.g. two gestures, no gate). Those are not inversions — with no gate there is nothing to swap, and the grid gives d1-d3 one button anyway. The count rose from 74 because column-aligning the buttons is what made them VISIBLE; they were unevaluable before. gSel (#54) is the fix.
PLN (Algolia) authored -
PLN: "we gotta ensure these are unified proper gig single service or package easy to maintain/add things to." tools/rig_units.py is now THE inventory. Three consumers, no re-typing: the Bridge panel imports SERVICES, gig-up.sh reads --ensure, and parvagues-rig.target is GENERATED by --target. Same shape as lcxl_grid.py (#97), for the same reason: the list was previously a hand-typed table in bridge/rig.py (5 rows, missing parvagues-bridge), a hardcoded loop in gig-up.sh (4 names), and whatever [Install] each unit carried. Adding a unit to the rig is one row. Two real bugs closed: * gig-up.sh only ever ran `systemctl start`, never `enable`. A start fixes tonight and changes nothing about the next login — exactly how tidal-ardour-autoroute sat `disabled` while every pre-gig check passed. --ensure does both verbs, per-unit boot policy: the reconcilers are "login" (they idle, hold no ports, make no sound), parvagues-sc stays "on-demand" because a login must not start SuperDirt. * `is-enabled` returning "linked" means NOT enabled — it reads like a healthy word. --status says so in words instead of printing systemd term. Also learned the hard way while doing this: a unit file symlinked from ~/.config/systemd/user into a FEATURE branch vanishes on `git checkout`, and `systemctl disable` on a linked unit deletes the symlink itself. Live infra files belong on master; both units are there now. systemctl --user start parvagues-rig.target # the single handle
PLN (Algolia) authored -
tidal-ardour-autoroute.service was disabled+inactive after launch: orbits 5-9 (SuperCollider:out_9..18) had NO link to their "Tidal 05..09" tracks, while 1-4 and 10-12 survived as leftovers from an earlier session. Enabled it; all 24 outs reconcile every 2s again. Root cause of the regression is that both reconcilers — this one and midi-autoconnect — existed ONLY as plain files in ~/.local/bin and ~/.config/systemd/user, unlike every other unit here which symlinks into tools/. Nothing was version-controlled, so a `disable` (or a fresh systemd user config) was unrecoverable and invisible to git. Now in the repo, with $HOME pointing back at it. Also started parvagues-sc-watchdog, which was linked but dead.
PLN (Algolia) authored -
PLN (Algolia) authored
-
- 22 Aug, 2026 1 commit
-
-
PLN opened fred_angie_bass, the first kit he tried, and found two bad loops immediately: ":0 starts, then silence, then starts again" and ":2 why is this not trimmed, silence before silence after?". Both had shipped as tier S with zero flags, one at grade 0.991. He was right, and it is worse than two files. Binning each loop's envelope on its own beat grid says 12 of 111 loops carry dead air — and 5 of 14 bass loops, 36%, which is why he hit it on his first click. angie:0 has beats 7-9 of 32 at -88 dBFS; bighen_bass:1 has a five-beat hole and 62.5% occupancy at grade 0.983. This is the third time this session that silence beat the grader, and the reason is the same each time, one level further down. Silence-as-optimum was fixed at export, then again in the finder before ranking, but every one of those checks is an AGGREGATE: file peak, whole-file RMS, max of the beat table. An aggregate cannot see a hole. A loud loop with a silent bar inside it passes all three. And kitcheck's dead-bar rejector looks at BARS, so a 2.5-beat gap straddling a bar boundary is invisible — angie:0's two bars read -18.3 and -15.9 dBFS while the beats between them read -88. Resolution was a correctness input and I had treated it as a detail. So this commit does not fix it, it measures it. beatocc.py bins the envelope on the loop's own beat grid (bpm and bars from the manifest, never re-estimated) and reports SHAPE rather than level: head and tail dead beats, longest interior dead run, occupancy, plus a beat map under -v. It exits non-zero when any loop has dead air, so it can go straight into kitgate once the thresholds are known. Thresholds deliberately not chosen yet. Occupancy floors have to be per family like the periodicity floors — marea_vox/06_breaths_1b at 25% occupancy is a breath, not a defect, while a bass loop at 62% is broken — and the head-silence question has three plausible answers (trim, rotate, reject) with rotation ruled out already, since angie:2's offset is 1.17 beats and _rotate_to_downbeat snaps to beats. Placing those floors by guess is how silence got graded A in the first place. #24 on the board carries the open decisions and the data needed for each. The Fred pack section is now marked DEFECTIVE rather than SHIPPED. It needs a re-cut once the shape check exists, and PLN's verdict stands as written: "tbh i dont think the loops are usable."
PLN (Algolia) authored
-
- 21 Aug, 2026 30 commits
-
-
TODO.md: the Fred section moves from "RESUME HERE" to shipped, with the gate numbers, the six-cuts-five-discarded history, and the audition-first list of 6 kitcheck advisories that are now waiting on PLN's ears rather than on any tool. completed-archive.md: #21 (La Cale) and #20b (the pack) as standalone entries. The learnings worth carrying: the rack had to BE `loopAt` rather than resemble it; a gate blocks on defects and reports properties, because blocking on a property is a gate everybody learns to ignore; `n` is the folder's index, never the manifest's; a rounded tempo is a stretched loop; and three tests that passed while proving nothing, which was most of the day's actual value.
PLN (Algolia) authored -
A parallel session in this shared checkout committed `027-the-instruments-were-the- broken-part.md` at 00:05, but 027 was taken at 17:34 by `027-the-label-and-the-lens.md` and 028 by `028-la-cale-and-the-empty-loop.md`. Renumbered to the next free slot; content untouched, only the filename and the heading. The captain's log is a sequence on purpose — it is the documentary's spine — so two entries sharing a number is not cosmetic. Worth noting that a shared working tree makes this collision structural rather than careless: neither session could see the other's number until both had committed. A worktree per session avoids the `git add` race but not this.
PLN (Algolia) authored -
Achievement log for the night before CosmicFest, and the board amendment that carries the resume points a cold session will need. The log's through-line is not the LED daemon; it is that FOUR separate instruments returned confident wrong numbers in one evening — `ps -o %cpu` summed as if it were instantaneous (it is a lifetime average; the main pulsar process read 2.1% while one child renderer burned 112%), `pw-top`'s ERR column read as current when it is cumulative (delta over 25 s: zero on every node), a shell path built as `live/$f` when `$f` already began `live/`, and a `grep -cE '[1-9] move'` that silently skips `10 move`. Three of the four returned a plausible number rather than an error, which is the whole problem: a wrong tool is rarely silent-and-empty, it is confident-and-slightly-off. Board amendment records what the next session must not redo: * The highlighter is EXONERATED by its own instrument — 5 RAF/s x 1.671 ms = 0.8% of a core, `markers 293 live, creates/destroys 0/s` over 4h42m. The marker leak is dead in the field. Do NOT re-open that hunt. * Five hypotheses for the ~112% renderer are already dead, each with its evidence, so they are not re-tried: editor-background (disabled), sound-browser rAF loops (all terminate), fork-per-LED (2.8 ms p50, bench p99 18 ms zero overrun), aseqdump pipe buffering (disproved on an isolated client). * gig-up is 5 blockers -> 1, and the survivor is PLN's decision to defer 20 column moves rather than renumber knobs hours before playing. * CosmicFest EXISTS (PLN's own words) but has no canonical www page. The date and venue must come from him, never backfilled from eb9221de. * Parked and flagged: 3205e7f unpushed in the pulsar-tidalcycles fork, sound-browser.js's 144 unvalidated insertions, backlog.md's +60 lines, and a one-line hello_fred.tidal stub.
PLN (Algolia) authored -
150 samples in 43 kits from Fred again..'s stem pack, staged at Samples/Fred/output/ and linked into Dirt-Samples, plus the page for deciding whether any of it is good. The pack took six cuts. Each discarded run was discarded because looking at its output found something: dead bars grading S, one rotation pass leaving 1 loop in 6 on a weak beat, then the big one — 24 of 153 loops were digital silence at -104…-75 dBFS, graded A and B, because every term in the loop rubric is vacuously perfect on nothing. Presence is a precondition now, at the grader and before the finder ranks. La Cale (armada/ui/kits.html) plays rate = dur / (bars x barlen), which is literally loopAt, so what you hear is what the rig does and the exported .tidal block is the same two numbers in Tidal's syntax. Python suite 112 -> 128; new vitest suite 24 cases.
PLN (Algolia) authored -
TASKS_DUMP.md opens by saying the Task API was unreachable, so the tree had to be rebuilt from git + memory + the task logs. That was true of the tools and false of the data: a near-limit checkpoint of this session had already written the whole TodoWrite state to .claude/RESUME.md — 112 items, 22 done, 7 current, 83 open. The curated epics in this file are about a fifth of the real board. Same failure shape as the three that cost this session hours: an instrument reported absence, and nobody asked whether it could have found the thing if it were there. Reconciled against 2ef09af6..c821bdf5, nine commits a parallel session landed in this checkout while the release thread was busy: * EPIC I, new — rig hardening and the Bridge. gig-up --converge and the gearbox were two owners of one setting with no arbiter, and ppd re-asserts on AC transitions, so a converge done on battery was silently undone by plugging in for the set. check-drift told PLN to delete his set the day before a gig. * CosmicFest prep across 24 track files and a new livecode_parade.tidal — but content/lives/2026/ has no cosmicfest page, so the date is NOT canonical and is not mine to invent. It is now the one blocking question on the board, because a date is what turns every H1 row into a deadline. Also flagged: four of the seven in-progress items are stale-active (the SoundCloud writer, the v4 re-cut, the mix/master/split stack, the per-track AT all landed), and A3-old + A4-Content-ID are superseded duplicates whose only live residue is two safety rules and one sanctioned playlist deletion. Merge candidates, PLN's call, not mine. The 112-item list is NOT durable — .claude/ is untracked and the checkpoint ref expires in about two weeks. Promoting it into this file is now a real task.
PLN (Algolia) authored -
Third round of "screenshot it rather than assume". The tag chips were absolutely positioned inside the waveform cell with a translucent background, so they covered the first ~15% of every envelope — which is the attack of the loop, the one part you look at to decide whether a cut landed on the beat. It read as a gap in the audio, and I nearly went looking for a bug in the envelope generator. Tags now sit on their own 11 px line under the waveform, truncated, with the CLAP confidence in the tooltip. `fred_marea_drums` reads correctly at a glance now: twelve loops, kick transients visible, three `kit` loops separated by @4:31 / @1:45 / @1:12.
PLN (Algolia) authored -
The headroom warning said "pulsar at N% — #7 (highlighter marker pool). This is the JITTER SOURCE; a Window:Reload is the stopgap". I read that, believed it, and spent a chunk of a pre-gig evening hunting a leak that is already fixed. What is actually true as of 2026-08-21: * Both documented marker leaks are FIXED on the running branch — `decorateCodeBlocks` tracks its markers in `blockMarkers` and destroys them (6e9f374, 2026-07-18), and `#createPositionMarkers` destroys the previous row markers before clearing the map (also 6e9f374; the cached-highlight pool was bounded in ba77c4f, 2026-08-03). * PLN's renderer was **3 minutes old and flat at 112%** across five 2 s samples. A leak GROWS. Accumulation cannot explain a fresh process holding steady, so whatever the burn is, it is not the marker pool. * `editor-background` is confirmed in disabledPackages, so it is not that either. The sound-browser's three `requestAnimationFrame(processChunk)` loops all terminate on `currentIndex < total`, so they are bounded too. Also worth recording: `ps -o %cpu` is a LIFETIME AVERAGE, not an instantaneous reading. My first two measurements ("122%", "130%") summed lifetime averages across the process tree and were meaningless; the honest number comes from sampling /proc/<pid>/stat utime+stime twice. The main pulsar process reads 2.1% while a child renderer burns 112% — a tree sum hides which process to look at. So the warning now reports the symptom, says explicitly that the known leaks are fixed and that a fresh hot renderer rules out accumulation, and points at `tidalcycles:highlight-stats` — the package's own live instrument (DOM writes/s, live markers, RAF rate, active events), which is the only thing that can attribute a burn INSIDE the renderer. A warning that names the wrong cause is worse than one that names none: it spends the reader's time defending its guess.PLN (Algolia) authored -
`loopAt` is exact, so a rounded tempo is a stretched loop — and the cheat sheet is where the number gets typed. It reported ANGIE and BIG HEN as "133 bpm" over files cut at 132.51, which plays every one of them 0.4% fast. Two decimals now, and each kit prints its own `setcps` line rather than leaving the ÷60÷4 to the reader: `loopAt` squeezes the file into N bars of the *current* cps, so the cps is half the instruction and omitting it is what makes the rounding bite. Same fix in La Cale's UI, where the transport read 123.05 next to a row reading 123.0 — one `fmtBpm` for the transport, the rack header and every row, because the same tempo printed two ways reads as a disagreement in the data.
PLN (Algolia) authored -
Run 6 through the whole gate. 150 samples in 43 kits across 9 tracks, staged at `~/Work/Sound/Samples/Fred/output/<kit>/` and symlinked into Dirt-Samples, so `s "fred_marea_drums" # n 3` resolves right now. restage --dry-run 0 of 43 kits (idempotent, as designed) grade re-run S×84 A×28 B×38 · manifest matches the audio on disk, 0 mismatches kitcheck 144 clean, 6 advisory, 0 failing · 150 of 150 distinct sounds n indices 43 kit dirs hold exactly their 150 manifest files bank-check "No shadowed bank names. Every `s \"name\"` is unambiguous." rights 43 of 43 third_party, with provenance — per-track release blocks MAREA is the ground truth: it declares `123BPM` in its filenames and the grid read **123.0**. PLEASEMAKEITBETTER read 20% tempo confidence and correctly shipped chops only. Nothing in the pack now sits below **-63.7 dBFS** peak, where run 3 had files at -104. **The gate had to be restructured before it would pass, and not by loosening it.** It blocked all 150 samples on 20 `near-silent` and 17 `mono-incompatible` files, none of which was broken. That is a category error I built in: a kit is gain-staged as a unit and deliberately never boosted, so its quiet members are quiet *on purpose*; and low interchannel correlation in a vocal double or a stereo pad is the width someone paid for. So the rule is now explicit — **the gate blocks on defects, reports properties.** Defects, which block: `clipping`, `empty`, and `mono-incompatible` **on bass only**, where summing to mono on a house PA kills the fundamental and nobody chose that. Properties, which are printed with the numbers that let PLN judge them by eye and go straight into La Cale's audition-first list: 20 near-silent (crest 14-34 dB — sparse, not silent), 17 wide (corr -0.35…+0.19), 2 marginal seams (+7.7 and +6.7 dB, where the threshold for an audible click is +20). Also dropped a false positive: `off-grid` on something the manifest calls a chop. A sub-bar one-shot has no bar grid to be off; `grade` guesses from duration, the manifest knows, so the manifest wins. The 6 kitcheck advisories are all weak bar periodicity on sustained material (pads, keys, synth) plus two MAREA drum loops at 0.08-0.12 against a 0.30 floor — bar-aligned but not repeating units. Those two are the first rows to audition. Blocking a pack on a property is not the cautious direction. It is a gate nobody can pass, which is a gate everybody learns to ignore. Suite 125 → 128.PLN (Algolia) authored -
PLN's set work for CosmicFest, plus one new track and three mechanical fixes found by the pre-gig gate. His edits, across 23 tracks: the ^43/^44 -> ^41 control migration continued by hand (37857225 retired gMask and freed ^41 for d1's gate), and real musical work alongside it — a `resetCycles` head on rose_rouge, new `octersubbus` and `squizbus` sends, a `slow 2 $ ply 2` rework of rose_rouge's ^54 line, gain/lpf/room adjustments, and sample swaps (`n "74"` -> `n "25"` on the Savoy break). New track: live/midi/nova/techno/livecode_parade.tidal. Three fixes from the gate, none of which move a control number, so nothing changes under his hands: * mute map — d10 in something_about_drums declared gM2; the authored map (tools/lcxl_grid.py) puts d4-d12 on gM3. fix-mute-roles.py --apply, then re-run to confirm it converged to 0. * crushbus 101 in ouais_je_funk was shared by d9 and d10, with DIFFERENT ranges (16->1.5 and 16->6) landing on one bus instance — last event wins, so the knob turned for one orbit was heard on the other. d9 moved to its own 91 per the <orbit><slot> convention. * crushbus 41 in ghosts_in_the_toilets was shared by d4 and d7 the same way. d7 moved to 71. pvlint goes 4 errors -> 0 on the setlist. NOT included, deliberately: the remaining 20 column moves (ghosts 10, something_about_drums 7, perfect 2, the_revolution 1). PLN: "migration i unfinishe dgrrr lets not finish it before set for now." Renumbering which physical knob does what in four tracks he plays tomorrow is a muscle-memory change, not a safety fix, and two of the moves cannot resolve mechanically anyway (one overflow -> gSel, one FIXME with no slot). gig-up stays honestly red on `surface grid intact` until it is finished after the gig.
PLN (Algolia) authored -
check-drift flagged 13 uncommitted .tidal files as "the stale-buffer signature" and its remedy section led with `git checkout -- <file>`. I nearly relayed that. All 13 were PLN's own work: the ^43/^44 -> ^41 gMask-retirement migration (37857225 freed ^41 for d1's gate) plus real musical edits — new octersubbus and squizbus lines, `slow 2 $ ply 2` reworks, gain 1.4->1.6, a sample change from n "74" to n "25". `git checkout --` would have destroyed a set the night before the gig, on the advice of a check that was confident and wrong. The script's own design comment already had the right principle: "drift is not control numbers changed, it is control numbers moved AWAY FROM THE GRID". Its per-file test just cannot see direction — it counts CHANGED ^NN lines (>= 4) and ANDs that with a global grid failure. Both were true here for innocent reasons: the grid is unaligned because the migration is HALF-APPLIED, and those files are the half that is done. Volume of change cannot separate a repair from a regression. Only direction can, and migrate-columns --plan already knows it: 0 pending moves for a file means its edits ARE the alignment. So: the DRIFT message now names both hypotheses with the one command that distinguishes them, the grid failure reports migrate-columns' pending count (20 here) and spells out that UNALIGNED is not REGRESSED, and the destructive remedy is behind an explicit "look at the diff first" with this incident as the reason. A gate that recommends data loss on ambiguous evidence is worse than no gate. Still exits non-zero, honestly: 9 knobs are genuinely out of column and 20 moves are genuinely pending. That is a real finding about an incomplete migration, and completing it is a decision about muscle memory that belongs to the person whose hands are on the desk — not something to apply the night before a gig.
PLN (Algolia) authored -
Two gate/monitor fixes found while clearing gig-up blockers the night before a gig. ABSENT IS NOT BROKEN. check-audio-graph hard-FAILED the moment Ardour's Master was on the internal codec, which made the whole pre-gig gate NO-GO while PLN was rehearsing on the laptop jack with the UMC in a bag. PLN: "no umc atm playing on the jack now. we need to be tolerant to various setups." He is right, and the repo already knows the rule — section 3 of this same script downgrades to warn when the interface is absent, and rig.py opens with "REPORT ABSENT AS ABSENT". Section 2 just never got the memo. Severity now depends on whether the UMC EXISTS. No UMC on the bus: warn, say plainly that the last hop cannot be proven until the interface is plugged in, and tell him to re-run at the venue. UMC present and Master still on the codec: that is the real regression this check was written for — Ardour restores its own saved ports and does NOT follow the PipeWire default sink, so it will happily ignore an interface sitting right there. Result: OK (3 warnings) instead of a NO-GO on a legitimate setup. A DEEP QUEUE IS NOT A BUFFER, IT IS A DELAY. PLN: "when i move faders up/down quickly, [it] lags by 500ms+, almost 1s, behind ahah". Chased this to the wrong end twice, worth recording. First guess was fork+exec per LED write — measured it: 2.8 ms p50, 8 ms p99, and --bench shows the coalescer at p99 18 ms with ZERO overrun, so the LED path was never the problem. Faders have no LEDs at all (row D), so a fader sweep does no LED writes; and the Pulsar HUD has no MIDI tap, so it cannot lag on MIDI either. Second guess was aseqdump block-buffering its stdout into a pipe (the exact bug this repo documents for Python) — disproved with an isolated aseqdump client fed by aseqsend: median 40 ms inter-arrival at a 25/s send rate, last event landing BEFORE the last send. It flushes per event. The real seam is the Bridge's SSE fan-out: `queue.Queue(maxsize=512)`. A fader sweep is ~400 CC/s, so 512 deep is 1.3 s of backlog — and once it fills it STAYS full, because drop-oldest holds the buffer AT capacity. Every event the browser renders is then 512 events stale, permanently. The drop policy was already right; the DEPTH was the latency. 512 was picked as "generous" and is really a latency budget nobody priced. Now 64 (~160 ms at that rate) — jitter absorption rather than a queue. Paired with the browser-side coalescing from 72937cc7, the consumer drains far faster than it fills, so it should rarely be reached. Verified: 79/79 bridge tests, check-audio-graph OK on the jack.
PLN (Algolia) authored -
Screenshotted the Fred view instead of trusting it, and it showed two things. The transport read **133 bpm on a 132.5 bpm kit**, because selecting a kit rounded its tempo. Harmless in a display, not harmless here: the rack sets `rate = dur / (bars × barlen)`, so 133 stretches every loop in that kit by 0.4% and the mismatch goes straight into the audio. Half-integer tempos are ordinary in this corpus. No rounding, and the "match this kit" button now compares to 0.01 bpm so it stops offering a value it already has. And `fred_bighen_bass` listed three loops all labelled `bass`, two of them also both "8 bars · 132.5 bpm" — the kit is named for the stem, so the stem name distinguishes nothing. The manifest already knew `start_s`, so rows now carry `@2:14`: where in the source track this loop was cut from, which is the thing you actually want when choosing between two loops off one stem.
PLN (Algolia) authored -
gig-up's "tools executable" gate has been NO-GO on this box, and the reason is the trap the repo already has a lesson about: chmod fixes YOUR tree, not the repo. parvagues-protect.sh and install-protect.sh both ran fine here and were recorded 100644, so a fresh clone gets two non-executable protect scripts — and the gate that catches it was being read as noise because it never went green. Fixed with `git update-index --chmod=+x`, which is the half that was skipped. lcxl-path.py (new in 7bda1ccd) had the same defect and gets the same treatment. Note the gate checks BOTH `-x` on disk and the index mode. Only the index half was failing, which is exactly the failure a local chmod hides.
PLN (Algolia) authored -
Captain's-log entry for the Fred pack and La Cale. The keeper: nobody wrote "prefer silence" anywhere. It fell out of five reasonable measures, each correctly implemented, none of which had an opinion about whether there was any audio in the file. A composite score is a specification of what you want, and every term you weight lightly is a loophole. Second keeper, the one that will save a future session: three tests today went green while proving nothing, and each fake test hid a real question. The finder test used the stem's own beat grid — which finds no beats inside silence, so no window was ever generated there and it passed with the fix disabled. The niceness test looked its hook up in the wrong scope, got None, and fell back to a no-op lambda. Break the fix, watch the test fail, put the fix back.
PLN (Algolia) authored -
`Deck.scheduleBar` mixed the decision (what plays, when) with the audio nodes, so the only rule in the rack with real musical consequence could not be tested without a browser. Extracted as `hitsInBar(voice, barIndex, bpm, swing, bufDur)`. The rule is that a bar-loop starts only on a bar that is a multiple of its own length. Break it and the rack still makes sound — it plays a 4-bar loop's bar 1 underneath the phrase's bar 4, so the groove turns around in the wrong place. That is not a crash, it is a musical wrongness you would find mid-set and probably blame on the sample. 11 cases, including the ones I would otherwise have got wrong: a negative bar index when the transport is nudged backwards, a phrase that runs past bar 8, a cleared cell at the second entry of a 4-bar loop, and the two asymmetries between the modes — a bar-loop is stretched to the DECK tempo (that is what layering means) while a chop never is (a stab is a stab at 90 or 174), and a chop is unconstrained by stride because it is sub-bar by definition. Confirmed the stride tests fail when the rule is disabled: 2 failed / 22 passed. Suite 13 → 24.
PLN (Algolia) authored -
Follow-up to 72937cc7. Having found that the Bridge panel could not see the LED watcher, I went looking for what else was watching the surface. Nothing was — and one of the things that claimed to be could not fail. THE FALSE-GREEN GATE. The pre-gig check for "LCXL -> SC" was: aconnect -l | grep -A3 "Launch Control XL" | grep -q "128:" `grep -A3` matches each of the THREE lines containing "Launch Control XL" and prints three lines after each; the last window runs off the end of the LCXL block and into the next client header, which is literally `client 128: 'SuperCollider'`. So the string "128:" was in the haystack whether or not a single wire existed. This gate has been passing since it was written, for a reason unrelated to what it measures. It would have said green with the desk connected to nothing. Proved rather than argued: recorded the live graph, stripped only the Midi Through -> SC edge from it, and ran both gates against the doctored file. Old gate PASSED. New gate reported "LCXL -> Midi Through, but Through does NOT reach SC" and exited 1. tools/lcxl-path.py replaces it by walking the graph. It has to: the real path here is TWO hops (LCXL 20:0 -> Midi Through 14:0 -> SC in2), so a direct-only test would be wrong in the other direction. It takes an optional recorded-graph file argument precisely so the gate can be tested for FAILURE without unplugging hardware at a venue — the reason the old one was never caught is that nobody could cheaply watch it fail. TWO UNITS NOBODY STARTED. lcxl-leds-watch and midi-autoconnect were both `disabled`, so neither came up at login; they ran only when started by hand. Both now join the auto-start loop. A NEW GATE. "The surface is wired" and "the surface is lit" are different daemons and different failures, and only the first was ever checked. The new "LCXL LEDs" soft check reads: if the board is on the bus, the watcher must be active. Deliberately conditional on the board — at a kitchen table an unplugged desk is normal and must not warn, but at the venue this is the difference between playing blind and not. Verified both new gates in BOTH directions: green as the rig stands, and red with the watcher stopped / the Through edge cut. A gate observed only passing is indistinguishable from the one this commit deletes.
PLN (Algolia) authored -
The Fred section now names every run that was thrown away and what found the fix, because that list is the actual work: 161 samples (dead bars), 153 (rotation), 153 (24 of them digital silence graded A/B), aborted (staging root), 129 (silence filtered at export only, so BIGHEN's tonal kit lost three loops instead of gaining three real ones). Run 6 is the first where the finder itself refuses a silent window. #21 moves to landed with its commands and its three honest gaps — the Strudel tab is text-only, swing is in the engine but not the UI, and a rack cannot be saved as a named preset.
PLN (Algolia) authored -
PLN, mid-cut: "you're hogging the system :(". Nine children at ~88% of a core each put the load average at **15.6 on 16 cores**, and the machine he performs and edits on became unusable. My fault twice over — the default was one subprocess per track regardless of the box, and nothing told the scheduler this work was background. Both fixed at the source rather than by remembering to pass a flag: * `--jobs` defaults to **half the cores, capped at 6**. It is the total that matters, not the track count, and each child peaks near 2 GB. * Children start at **nice 19 and SCHED_IDLE** via a preexec hook, so they only run when nothing else wants the CPU. The nine still saturate the sixteen cores; the editor now wins every scheduling decision, because nothing waits on this work but me. (The live run was reniced in place rather than killed — eight minutes of analysis is not worth a restart.) The test for that hook is worth a note, because the first version of it was fake: it looked the hook up through `_fan_out.__globals__`, where a nested function does not live, got `None`, fell back to a no-op lambda, and passed. Same shape of mistake as the finder test earlier today. So `run_at_idle_priority` is module-level now and the test asserts on a **real child's** reported nice and scheduling policy — verified to read `0 0` without the hook and `19 5` with it. Also in here, from the same run: the presence gate I added to the finder was doing a full `max(abs(window))` per candidate — at 8 bars that is ~700 k samples times thousands of windows, which is most of why this run was slower than the last. It now reads a per-beat peak table computed once per stem. Exact, not approximate: the inter-beat segments partition `[int(times[i]*sr), int(times[e]*sr))` precisely, so it visits the same samples — with a test that checks the two agree for every window rather than trusting the argument. And `kitgate --link` now takes each kit's staging root from the manifest instead of `publish.samples_root()`. A pack cut with `--samples-root` lives beside its source, so the default root would have looked in the wrong place and called every kit missing. Suite 122 → 125.PLN (Algolia) authored -
PLN, the night before a gig: "i dont see midi feedback visuel anymore on the LCXL fix this first plz". The hardware was fine. The daemon was gone. The journal had the whole story and nobody was reading it: lcxl-leds --watch: no LCXL sequencer port; retrying in 30s (x8) Stopping LCXL LED watcher... Stopped LCXL LED watcher. The board was unplugged, the watcher retried, then it was STOPPED — and `Restart=always` does not resurrect a unit somebody stopped. On replug the board came back and the daemon did not. Three independent holes let that become a silent, session-long dark surface: 1. `lcxl-leds-watch` was MISSING from rig.py's SERVICES table, so the Bridge panel showed a fully green rig over a dark board. That is precisely the failure the module's own docstring opens with ("A GREEN UNIT IS NOT SOUND"), one table row away. It was also in neither gig-up.sh nor converge: NOTHING on this box checked whether the surface was lit. 2. The unit was `linked`, not `enabled` — it never started at login. It only ever ran because something started it by hand. 3. `midi-autoconnect` was `disabled` too. The wiring it enforces (LCXL 20:0 -> Midi Through 14:0 -> SuperCollider in2) happened to be intact, so knobs still worked and the fault stayed invisible. But the prescribed fix for the LED stall IS a replug, and a replug drops those connections with nothing to re-apply them. The two failures compound: the remedy for one silently triggers the other, mid-set. Fixed: both units enabled, and the watcher is on the panel with a state that `active` cannot express. It retries forever by design, so "alive" is not "lit" — _leds_state() cross-checks the board via procfs and reports the third case honestly. No board is ABSENT, not broken; board present with the daemon down is "plugged in but DARK, start this", the one combination that was silently wrong. All five services now read green for a reason each, not by omission. Also, the daylight ramp. PLN: "maybe brightness can be leveraged, top brightness always would make more readable signals even in day perfs." The six-step unipolar ramp spent THREE steps on dim shades — exactly the budget that vanishes outdoors. The replacement keeps five steps and puts every one at full brightness by using the two mixed hues the old ramp never touched: red 15 (g0,r3) -> orange 31 (g1,r3) -> amber 63 (g3,r3) -> yellow 62 (g3,r2) -> green 60 (g3,r0) Every value has a component at maximum, so nothing depends on a brightness difference to be legible. It costs one step and gains three readable ones — the same trade he already made for the DJ filters in July ("i agree on clarity > resolution"), which have been all-full-brightness ever since. This just brings the unipolar knobs in line. LCXL_DIM_RAMP=1 restores the old ramp, so a dark-stage revert is one env var and a restart, not a code change. And the gear's MIDI monitor: "super noisy ... cant we do way more dense". "Control change" spent 14 characters saying "CC", and a single fader sweep prepended ~100 near-identical rows. Events now abbreviate (CC/ON/OFF/PB/...) and consecutive events from the same control coalesce into ONE row that updates in place with a x-count — a 100-event sweep is one line, one DOM write per event, no node churn. Row height 20px -> 15px on top of that. Verified: 79/79 bridge tests pass; /api/rig reports all five services up with per-service proof; ramp asserted to have max(g,r)==3 at every step.PLN (Algolia) authored -
`armada/ui/README.md` was still the stock `npm create vite` text after four pages had been built in it, so the one file a cold reader opens first said nothing true. It now lists the five pages and what question each answers, the commands, and the two things that are easy to get wrong: audio is *mounted* through `audio-mounts.json` (read by both vite dev and serve.py, so a URL works in both — a UI that only works under `npm run dev` is a demo), and `kits.json` is generated, uncommitted, and written to `dist/` as well as `public/`. The `packs/` README now says where a pack's audio is staged, since it is no longer one place: kits cut from someone else's stems live beside their source (`Samples/Fred/output/<kit>/`, `--samples-root`) so the pack stays one movable, deletable thing, while ParVagues' own kits sit directly under `Samples/`. The bank name is the directory name either way, so `s "<kit>"` cannot tell the difference. Then I took a headless screenshot of the page instead of assuming it rendered, which found three things a typecheck cannot: a legacy kit's filenames are long enough to truncate with nothing to recover them from (now `title`), the flag filter rendered as a dead `! 0` on every kit with no flags (now hidden), and the empty-rack text ran the full width of the screen.
PLN (Algolia) authored -
Rejecting silent loops at export was the right fix in the wrong place. BIGHEN's strings stem plays in two sections and rests between them, and all six of the windows the finder ranked highest sat inside a rest — so filtering at the gate took `fred_bighen_tonal` from four silent loops down to one real one, when it should have gone to four real ones. `fred_bighen_fx` disappeared entirely for the same reason. The composite is degenerate on silence in the finder too, and for the same reason it was in the grader: a rest is maximally self-similar in the SSM, its seam is immaculate, its zero crossings are trivial. So presence is checked before ranking, on the window itself, sharing `grade.THRESH["empty_dbfs"]` by import so the two stages cannot disagree about what silence is. The test for this took two attempts, and the first one was the interesting one. A stem gated on its own beat grid CANNOT reproduce the bug: `beat_grid` finds no beats in the silence, so no window is ever generated there, and the test passed with the gate disabled — proving nothing. The bug needs a grid derived from ANOTHER stem, which is exactly how `stempack` calls `analyze_stem` and exactly why it does so: sharing a rhythmic track's grid gives every stem windows across the full duration, including the parts where that stem is not playing. Verified the test now fails with the gate off and passes with it on. Suite 119 → 122.
PLN (Algolia) authored -
`public/kits.json` is a 1.2 MB snapshot of audio that lives outside the repo, and `.kitindex-cache.json` is derived from file mtimes — both regenerate in seconds and neither belongs in history. @strudel/web went in while working out whether the sequencer should be a Strudel playground. It should not: the deck already plays the rack with `loopAt` semantics, and a second audio graph would only add a way for the two to disagree about what the same cells mean. The Strudel tab emits text for strudel.cc instead. So the package comes back out — a dependency nothing imports is a false statement about what the project needs, and it would have shipped in the lockfile forever.
PLN (Algolia) authored -
PLN asked for an SPA to audition the cut samples, "click-based looping not just strudel coding". La Cale — `armada/ui/kits.html`, the hold, where the cargo gets checked before it goes on stage. The reason it needs to exist is that a grade in a manifest is a claim, and this week the claim has been wrong twice in ways no report caught: a loop that fades to silence grades S, and 24 loops of pure digital silence graded A. Both are obvious in two seconds of listening and invisible in a table. **Auditioning is two different tests, so it is two buttons.** Play-once gives you the file untouched — attack, tail, what it actually is. Loop plays it seamlessly at rate 1, which is the only way to hear the seam, and the seam is the thing that decides whether a loop is usable because it is the sound the audience gets every bar. One AudioBufferSourceNode with `loop = true`, so drift cannot exist and nothing is resampled between the file and the speaker. **The rack is where a kit is actually judged**, because the questions that matter — do these two loops sit together, does this vocal survive on that break, is the bass really at this tempo — cannot be answered one file at a time. It schedules every voice from a look-ahead clock against an absolute grid, at rate = buffer.duration / (bars * 4 * 60 / bpm) which is exactly `loopAt bars`. So the layering you hear is the layering the rig plays, and the exported block is not an approximation of the session — it is the same two numbers in Tidal's syntax. Scheduling per bar rather than letting nodes free-run matters more than it sounds: the cutter's bar tolerance is 1 ms, which is 60 ms of skew after a minute. The grid means what Tidal means. A bar-loop row has cells only on multiples of its own length — a 4-bar loop in an 8-bar phrase has two entries, not eight, because firing it on bar 3 plays its bar 1 under the phrase's bar 4 — and those cells become `mask "t f"`. A chop row is 16 steps in a bar and becomes `s "x ~ ~ x …"`. Three things the export gets right that are easy to get wrong, all pinned by tests: the fader is linear but Tidal's gain is quartic, so 0.5 exports as `gain 0.84` and copying it across would be a 12 dB error; families land on their own `dN` with no two voices sharing an orbit; and the mask samples the grid on the loop's stride, not every bar, which otherwise silences an entry that is switched on. Deliberately not wavesurfer, though the project already depends on it: a kit page shows a hundred rows, and a hundred instances means a hundred fetches and decodes to draw something 200 px wide. `kitindex.py` precomputes a 128-point envelope per sample, so a row costs one `<svg>` and no network — audio is fetched only when something is played. Same ship-the-index instinct as the tide-table. `kitindex.py` merges two sources on purpose. The FOLDER is the truth about what exists (via `pvbanks.playable`, so it agrees with kitgate and the watcher), which is why the 111 kits that predate the Foundry are browsable too. A cut MANIFEST is the truth about what a file MEANS — tier, family, bars, bpm, CLAP tags — and none of it is re-derived here, so the page and the gate cannot disagree. `n` comes from the folder listing, never the manifest order, because `n` is what SuperDirt plays. Two staleness traps closed while building it: the envelope cache is keyed on path+size+mtime, so a restaged kit misses it (a stale envelope over fresh audio is a lie that looks like data), and the index is written to `dist/` as well as `public/`, because `npm run build` copies public/ once and the LAN page would otherwise show an index from before the last cut. Also: the dev server hardcoded `Content-Type: audio/flac` for everything under /audio. The masters are FLAC; the kits are WAV. Now by extension, matching serve.py's `guess_type`. 13 vitest cases on the export; `npm test`. Verified end to end through the real path — `serve.py --dir ui/dist` returns 200 on /kits.html and 206 `audio/x-wav` on a Range request through the new `samples` mount.PLN (Algolia) authored -
PLN, 2026-08-21: "i dont use the webui most of the time, tray indicator is the only ui i use for parvagues gear bro". The gearbox shipped with an engine, a CLI and a web panel — and its one everyday surface still said Silent/Cool/Standard/ Extreme/Normal, still wrote governor/EPP/platform_profile itself through `sudo perf-audio`, and was DISABLED at login. So the surface he uses was the one surface that had not moved. What this does * tools/bridge/gearbox.py — the client both faces speak. Names the three gears and the three trims once, reads the persisted state unprivileged, and builds the argv for a switch. rig.py's gear() now delegates to it, so the tray and the web panel cannot drift on what a gear is called. * perf-tray.py — the mode list is gone. In its place: gear (off/standard/ aggressive) and trim (quiet/auto/cool) as two INLINE radio blocks under their own headers, because the separation is the design. A reader who sees one flat list keeps believing there is a single "how fast" dial and that quiet and cool are two words for it; two headers teach the model every time the menu opens. * It no longer writes any thermal knob itself. Switching goes `sudo -n` to the arbiter, which is the point — a tray reaching around it would rebuild the four-writer mess GEARBOX.md removed, in one click. * The desired-mode bookkeeping is gone with it. The gearbox persists the gear and re-asserts it from a udev hook on power-source changes and a system-sleep hook on resume; a second bookkeeper in the tray could only ever disagree. * The checked gear is read from the FILE, never inferred from sysfs. Inference (perf.detect_mode) cannot tell a deliberate gear from power-profiles-daemon having moved the same knobs, which it does by design since the gearbox cooperates with ppd rather than masking it. * "Gear ▸" (the apps and services) is now "Rig ▸": gear is the thermal lever now, and the web panel already calls that collection the rig. * perf-tray.service was never tracked — it lived only in ~/.config. Now in the repo, so the unit and the script it runs can no longer drift apart. The icon carries the lever, so it reads with the menu shut Width = gear, colour = trim, drawn as the badge's border. One element for two axes because the panel renders this at ~22 px and any design with two separate marks turns to mush there. Width is the cue that has to survive a dark room and a peripheral glance ("am I in the gig gear?" is the mid-set question), so gear gets shape and trim, which is set-and-forget, gets colour. Off draws no ring: a bare badge is the honest picture of nothing steering the machine. First attempt used 2 px / 5 px, which looked right in a mockup and VANISHED on the panel — 64→22 px scaling turns a 2 px stroke into 0.7 px of grey. Rendering every combination at 20/24/32 px before believing it gave 6 px / 11 px, and then forced the temperature numeral and the sparkline to shrink with the ring, since an 11 px border over a 30 px numeral clips the digits at exactly the size where aggressive is the gear whose temperature you most want to read. Verified on hardware, through the real menu over its D-Bus interface (Plasma's own protocol), not by calling the Python directly: * live tooltip reads "gear: Standard · Cool · cap 15/35W" * the menu exports two radio groups, correctly checked, headers live * clicking Aggressive -> GEAR=aggressive, RAPL 45 W / 115 W * clicking Standard -> GEAR=standard, back to 15/35 W * clicking Cool -> TRIM=cool, fans 0 -> ~2900 rpm * a CLI-side gear change is picked up by the tray within one refresh * enabled and running under graphical-session.target, so it survives logout Harness caveat worth writing down: the very FIRST dbusmenu Event sent to a never-opened menu landed on the wrong item (asked for Aggressive, got trim Auto). Every subsequent one mapped exactly, from both dbus-send and gdbus. Reading it as provisional ids on a menu Qt has not yet had to show — a human cannot click an item without opening the menu first, which is what the later probes did. 18 tests for gearbox.py (79 in the bridge suite, all green). They defend two things, neither arithmetic: the sudoers contract (the argv IS a security boundary — `--trim cool` must stay two words, and `performance`/`--boot`, both real helper verbs, must never be reachable from a face that only knows gears), and the honest-unset rule (never-engaged is not the same state as off, which is why off had to become an explicit gear).PLN (Algolia) authored -
kitgate blocked the Fred pack and the reason turned out to be much worse than the flag it raised. Measuring the flagged files: 24 of 153 shipped loops have a peak between **-104 and -75 dBFS**. They are not quiet. They are digital silence. And they graded **0.707 to 0.896 — tier A and B**. The mechanism is that every sub-score in the loop rubric is vacuously perfect on nothing. The seam between two silences is immaculate. The DC of silence is exactly zero. Its zero crossings are trivially fine. Its bar length is whatever you cut it to, so it is bar-exact by construction. Level carries 10% of the composite, so a silent window scores 0.90 × perfect + 0.10 × nothing ≈ 0.90 and comes out tier A. The composite had a **degenerate optimum at "no audio at all"**, and the finder, doing exactly its job, walked into it: `fred_marea_bass` shipped three loops and all three were silence, from a stem measuring -22 dBFS RMS at 54% activity. Nothing downstream could catch it either. `_has_dead_bars` compares bars to EACH OTHER, so it finds a loop that fades out and is blind to one that was never there — a uniformly silent clip has a perfectly flat bar profile. This is the same trap as the fade-out loops grading S, one level deeper. So presence stops being a weighted term and becomes a **precondition**: peak below `empty_dbfs` ⇒ flag `empty`, grade 0.0, tier D, and the existing MIN_TIER filter drops it at export. The floor is -60 dBFS, and it is placed rather than tuned: the pack's peaks run -104…-75 and then jump to -47.1, a **28.4 dB empty gap**, so -60 sits in clean air with nothing near it. Three details the measurements forced: * **Presence is measured on the CHANNELS, not the mono sum.** An anti-phase stereo pair sums to exact zero; calling that "empty" would both mis-report it and suppress the mono-incompatible flag, which is the one useful thing to say about it. (Caught by an existing test, which is why it is an existing test.) * **`near-silent` (RMS < -45) is advisory and now excludes `empty`.** RMS on sparse material measures how much silence it contains, not whether it is silent — a hat loop peaking at -14.7 reads -48.8 RMS. Two questions, two lenses. * **`mono-incompatible` no longer fires on silence.** Two independent noise floors are uncorrelated, so silence reads as maximally mono-incompatible; four of MAREA's flagged vox loops were simply empty. `kitcheck` asks the presence question too, from its own code path, sharing the one constant by import so the two lenses cannot drift apart. Also: restage was not idempotent. Export stages in float and writes PCM_24, so the peak read back off disk misses the ceiling by a quantization step, measures a hair hot, and asks to be staged again — by -0.00003 dB, every run, requantizing every file in the kit each time. `DEAD_BAND_DB = 0.05` (inaudible, four orders of magnitude above the quantization floor). The dry run now reports 0 of 42 kits, which is what it was supposed to say in the first place. 7 tests; suite 115 → 119.
PLN (Algolia) authored -
The cheat sheet numbers `n` by enumerating the manifest sorted by filename. SuperDirt numbers it by enumerating the *folder* sorted by filename. Those two agree only for as long as the folder holds exactly the manifest's files — and `export_track` writes into a kit dir it never empties. So re-cutting a pack is quietly unsafe. If a loop that was `00_kick_2b.wav` last run comes out as `00_kick_4b.wav` this run, both files are on disk, the folder listing gains an entry at position 0, and every index from there on addresses the wrong sound. Nothing else notices: the manifest is right, the grades are right, every file is a good loop, and the cheat sheet you paste from looks correct. You find out on stage. Noticed while looking at the run-3 kits and asking whether runs 1 and 2 had left anything behind (they hadn't — all 58 files carried run-3 mtimes). The absence of the bug today is not the absence of the bug. check_indices compares the two listings **index for index**, not as sets, so it distinguishes the two severities the report needs to separate: an orphan that sorts last pollutes the bank but shifts nothing, while one that sorts early breaks addressing from that point on — and it names the index where it breaks. `--prune` deletes what the manifest does not list, scoped to manifest kit dirs and to audio files only. Listing comes from `pvbanks.playable`, so "what SuperDirt will load" keeps one definition across kitgate, bank-check and the watcher — a checker and a loader that disagree here is the whole failure mode. 3 tests (clean / shifting orphan / trailing orphan); suite 112 → 115.
PLN (Algolia) authored -
#20 lands; #21 (auditioner SPA), #22 (acapellas + demucs catches), #23 (audit the existing 163 kits) opened with enough detail to start cold. The Fred pack's finish sequence is written out step by step because the final run is still in flight.
PLN (Algolia) authored -
Periodicity measuring length then role, dead bars grading S, no headroom against amp=gain^4, and 1 loop in 6 opening on a weak beat. Plus the two bugs the checks caught in my own work, which is the argument for having them.
PLN (Algolia) authored -
Chasing "does a shipped loop actually start on its downbeat" and nearly filing a false alarm on the way. First measurement said 17% — but it scored 16 slots with no full-band term while the rotator scores bars*4 slots WITH one, so the disagreement was the instrument's. Factored the scorer out (_downbeat_slot_scores) so the rotation and its verification cannot use lookalike measures, and re-measured with the real one. The bug is real and smaller than the first number: over 60 shipped bar-loops, slot 0 was the best slot in 29 and in the WORSE HALF of slots in 10. Median miss was 0.032 — i.e. usually a near-tie — but one loop in six genuinely opens on a weak beat, which is exactly the "timed ok but cut BCDA/DABC" complaint from 2026-07-11 that this function was written to fix. Mechanism: the roll point is zero-crossing snapped by up to ~10 ms against a 20 ms attack window, and between two near-tied slots that is enough to flip which one wins. On clean synthetic material rotation is idempotent (pass 1 finds slot 4, pass 2 finds 0); on real music the tie-breaking is fragile. So it now checks its own result and repeats, up to 3 passes, keeping a pass only if the measured miss improves. Safe because rotation is free: two rolls compose into one and the guard cannot oscillate. Best slot 29 -> 46 of 60, worse half 10 -> 1, median miss 0.032 -> 0.000. Default is 3 rather than opt-in: it is strictly better and applies to every caller, including export_take, so the GUI's forge gets it too. 112 tests.
PLN (Algolia) authored
-