Commit 0f271843 by PLN (Algolia)

merge: the Fred pack and La Cale

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.
parents 1e2bc3d9 ee8d4ac2
---
log: 027
title: "The label and the lens"
date: 2026-08-21
task: "Foundry #20 batch-explore many sources"
tags: [tooling, sampling, foundry, analysis]
shareable: true
---
## Cap (what & why)
Fred again.. dropped his stems. 36 GB of them — 9 tracks split by the producer himself
into 6–13 named stems each (`MAREA MIX10 123BPM KIT STEM.wav`), plus 13 acapella /
instrumental pairs. Cutting that into TidalCycles kits by hand in Audacity is exactly
the drudgery the Foundry's loop finder was built to kill. So: point the finder at
somebody else's stems and find out whether it actually works.
It mostly did. What it got wrong is the interesting part.
## Manœuvre (how)
The finder existed and was good — beat grid, Foote novelty, verify-rerank on the true
post-snap seam, downbeat rotation, an export path that guarantees bar multiples. What
did not exist was a way to feed it a folder somebody else had already separated:
`find_takes` keys stems by demucs' four names, a dict that collides on KEYS1+KEYS2 and
an export filter that would have silently dropped every stem in the pack.
So `engine/stempack.py` (the batch driver), `engine/roles.py` (the pack's ~40 role
tokens → a family, then verified by measurement), and `engine/kitcheck.py` (the audit
of what `grade` structurally cannot see). One kit per track × role family.
Then: run it, look at what came out, fix what was wrong, run it again.
## Prise (findings / artifacts)
- `tools/foundry/engine/{stempack,roles,kitcheck}.py` + `kitgate.py` + `restage.py`,
26 new tests, suite 86 → 112.
- MAREA declares its tempo in its filenames, so it is free ground truth. Final grid:
**123.0 bpm against a declared 123 — 0.0 % off.**
- **96 kHz was breaking the finder.** At native rate it returned 123/123/124/**119**/
**128.1**/**128.5** bpm with a 0.661 top score; resampled to 44.1 kHz first, 123.0–123.1
on every candidate at 0.859, in a third of the time.
- Every loop was being cut at its own local tempo — ME's grid read 129.2 while both its
shipped loops were cut at 150.5. Bar length now comes from one robust track tempo
(the mode of the local-BPM histogram) with a confidence, and a track with no usable
pulse ships chops only.
- Chops beat loops mechanically every time (0.993–0.999 vs 0.841–0.881), so the vocal
kit came out as three stabs and no loops. Modes now rank in separate pools.
- The probe was looking in the wrong place with the wrong lens, and threw away good
material both ways — see below.
### Then four more, found by measuring the output instead of reading it
- **The periodicity check was measuring loop length.** Autocorrelation at lag *k* has
only *N−k* overlapping frames; normalised by `ac[0]` it scored a *perfectly periodic*
click track 0.418 / 0.623 / 0.754 at 2 / 4 / 8 bars. Then the floor had to go per role:
medians drums 0.754, fx 0.549, tonal 0.214, bass 0.173, **vox 0.087**. One threshold
condemned 12 of 20 vocal loops for the crime of being vocal.
- **Dead bars grade S.** A 4-bar loop whose last two bars are a fade is silent-to-silent
— a perfect seam — at an exact bar length. Ten of the first 161 cuts were that. Now
rejected at export, not reported afterwards.
- **Kits had no headroom.** MAREA's ALL DRUMS and KIT both peak at **+0.2 dBFS**, and
`amp = gain^4`, so `# gain 1.2` is +7.6 dB and the orbit clips the moment you touch it.
Kits now land at −6 dBFS as *one gain per kit* (per-file would flatten a sparse hat
against a full drum bus) and never boosting.
- **1 loop in 6 opened on a weak beat.** The exact "timed ok but cut BCDA/DABC" complaint
from July. The roll point is zero-crossing snapped by up to ~10 ms against a 20 ms
attack window — enough to flip two near-tied slots. Rotation now checks its own result
and repeats: best slot 29 → 46 of 60, worse half 10 → **1**.
And two bugs the *checks* caught in my own work, which is the argument for having them:
`kitgate` re-grades the shipped files instead of trusting the manifest, and immediately
found that staging applied its gain after grading, so the recorded number described audio
that was never written. Separately, a report section I added never rendered — a
`str.replace` whose anchor did not match, and the one patch in that file where I had not
asserted on the anchor.
## Sel (the shareable learning)
**Two ways to be wrong about a stem, and they pull in opposite directions.**
The house rule here is *never infer a sound's role from its name — validate by
analysis*. It is a good rule, learned the hard way. But applied without thinking it
becomes *the measurement always wins*, and that is a different claim, and it is false.
Measurement said MAREA's ALL DRUMS stem was **bass** — lf_frac 0.86, centroid 216 Hz,
identical to a real bass stem on those features, because a modern kick carries most of
a drum mix's energy. It said the sparse CLAP stem was **tonal**. It said the rhythmic
KEYS2 was **drums** at 5.1 onsets/s. Trust it blindly and three stems go into the wrong
kit, silently, with a confident measurement behind each one.
The resolution is not to pick a side but to notice they are decisive about *different
things*. Presence is measured — a stem under −40 dBFS peak has nothing in it, full
stop. Bass is measured — "all energy under 150 Hz, nothing above 2 kHz" is unambiguous.
Everything else keeps the producer's label, because the cheap features genuinely cannot
separate a rhythmic rhodes from a drum bus. Disagreements get reported, not resolved.
The rule was never "don't trust the name"; it was "don't *infer* from the name" — and a
human's statement about their own stem is evidence, not inference.
**And the lens has to fit the thing.** Usability was gated on overall RMS. ANGIE's
HIHATS stem reads −48.8 dBFS RMS and got binned as near-silent — but it peaks at −14.7
and is a perfectly good hi-hat stem. RMS on sparse percussion measures how much silence
it contains. Worse, the probe sampled evenly-spaced windows to save time and landed in
the gaps: it read that stem at −60.9 / 1.4 % active where the full file says −48.8 /
9.7 %, and missed PAD STUFF's material entirely, 0.0 % against 6.9 %. Look in the wrong
place with the wrong instrument and you will get a number, and it will be wrong twice.
**Bonus, same shape:** onset *count* is not a percussiveness measure. librosa's
peak-picker finds peaks in a flat envelope — a steady 50 Hz sine yields 84 onsets in
six seconds. HPSS was the obvious fix and is worse: it hears a modern kick as harmonic
and calls a drum bus 9 % percussive.
**And check the instrument before you accuse the rig.** The first measurement of "do
loops start on their downbeat" said 17 %, which looked like a broken feature. It scored
16 slots with no full-band term while the rotator scores `bars×4` slots *with* one — the
disagreement was mine. Re-measured with the rotator's own scorer, factored out so the two
cannot drift apart, the real number was 48 % best-slot with a median miss of 0.032: mostly
near-ties, but a genuine 17 % starting on a weak beat. The bug was real *and* the first
number was wrong, which is the annoying case.
## Hameçon (hook)
"The machine told me the drum stem was a bass. It was right about every number and
wrong about the sound." — three ways to measure a stem and be confidently mistaken,
and the one rule that sorts them out.
## Sillage (what it unlocks)
Foundry #20 lands: any folder of already-separated stems is now a batch away from being
a playable kit. Next: the 13 acapellas (chops mode, already 44.1 k), and the same driver
pointed at the 27 demucs catches sitting in `~/Downloads/separated`. `kitcheck`'s dead-bar
and periodicity tests are general — they belong in the tierlist pass over the whole
Samples corpus, where "grades S, useless" is presumably not rare.
---
log: 028
title: "Silence graded A: the day the grader's best loop was nothing at all"
date: 2026-08-21
task: "#20 finish the Fred pack · #21 La Cale"
tags: [tooling, sampling, foundry, ui, measurement]
shareable: true
---
## Cap (what & why)
Finish the Fred again.. stem pack into loadable Tidal kits, and build the page PLN
asked for to hear whether they are any good — *"click-based looping not just strudel
coding"*. The pack was "done" three times before it was done.
## Manœuvre (how)
The gate blocked the pack, so I measured the files it flagged instead of adjusting the
flag. 24 of 153 shipped loops had a peak between **-104 and -75 dBFS**. Not quiet.
Digital silence. Graded **0.707-0.896, tier A and B**.
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 length is a bar multiple by construction. Level carries 10% of the
composite, so silence scores 0.90 × perfect + 0.10 × nothing 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. The dead-bar check 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.
So presence stopped being a weighted term and became a **precondition**. The floor was
placed, not tuned: the pack's peaks run -104…-75 and then jump to -47.1, a **28.4 dB
empty gap**, so -60 dBFS sits in clean air.
Then the second half of the same bug. Filtering 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
windows the finder ranked highest sat inside a rest — so `fred_bighen_tonal` went from
four silent loops to **one** real one when it should have gone to four. The finder's
composite is degenerate on silence too (a rest is maximally self-similar in the SSM), so
presence is now checked **before** ranking, sharing one constant with the grader by
import.
Meanwhile: **La Cale**, `armada/ui/kits.html`. The rack schedules every voice at
`rate = dur / (bars × barlen)`, which is literally `loopAt`, so what you hear is what
the rig plays and the exported `.tidal` block is the same two numbers in Tidal's syntax.
## Prise (findings / artifacts)
- `engine/grade.py``empty_dbfs = -60`, presence measured on the **channels**, not the
mono sum (an anti-phase pair sums to zero and is not empty).
- `engine/loops.py` — presence gate before ranking, via a per-beat peak table so it is
free; `engine/kitcheck.py` asks the same question from its own code path.
- `kitgate.py` — 5th check: does `n 3` on disk address the file the cheat sheet names.
- `armada/ui/kits.html` + `src/kits/` + `tools/foundry/kitindex.py`.
- Python suite 112 → 125; new vitest suite 24 cases.
## Sel (the shareable learning)
**A composite score is a specification of what you want, and every term you weight
lightly is a loophole.** Nobody wrote "prefer silence". It fell out of five reasonable
measures, each correctly implemented, none of which had an opinion about whether there
was any audio. Presence is not a quality you trade against seam cleanliness — it is a
precondition, and preconditions belong outside the weighted sum.
**Three tests today passed while proving nothing**, and finding that out was most of the
value. 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 gate disabled —
the bug needs a grid shared from *another* stem, which is exactly how the real caller
works. The niceness test looked its hook up in the wrong scope, got `None`, fell back to
a no-op lambda, and went green. Now the habit is explicit: **break the fix, watch the
test fail, put the fix back.**
**And a background job that takes the desktop down is not a background job.** Nine
children at 88% of a core each put the load average at 15.6 on 16 cores, on the machine
PLN performs on. `nice 19` + `SCHED_IDLE` and half the cores by default — the work is
genuinely background, nothing waits on it but me.
## Hameçon (hook)
"My sample cutter's favourite loop was four bars of nothing — and it scored an A. Here
is why every one of its five measurements agreed."
## Sillage (what it unlocks)
The presence precondition is general, so #23 (auditing the 111 kits that predate the
Foundry) will find the same thing in hand-cut material. La Cale is the missing feedback
loop for every future cut: PLN can now hear a kit in seconds and the manifest's claims
get checked by ear instead of accumulating.
......@@ -1628,3 +1628,78 @@ cannot validate a stem-based lens — that gap should be closed before attempt f
**Deps.** Superseded in practice by the boundary lab (#B2), which made the ear call cheap
enough that the detector stopped being necessary.
## #20 — Batch the Foundry over producer stem packs
**Description.** Fred again.. dropped 36 GB of stems — 9 tracks split by the producer
himself into 6–13 named stems each (`MAREA MIX10 123BPM KIT STEM.wav`), plus 13
acapella/instrumental pairs. Cutting that into TidalCycles kits by hand in Audacity is the
drudgery the loop finder was built to kill. Goal: `fred_XXX` kit dirs, one per track, each
holding loops cut from that track's stems — and find out whether the finder actually works
on someone else's stems.
**Done.** Branch `claude/fred-kits`, 14 commits. `engine/stempack.py` (batch driver),
`engine/roles.py` (~40 role tokens → family, verified by measurement), `engine/kitcheck.py`
(the audit `grade` structurally cannot do), `kitgate.py` (the one gate before loading),
`restage.py` (gain-staging for kits already on disk), `--report-only` (rebuild views from
the manifest), `packs/` (provenance, not audio). Suite 86 → 112. Write-up:
`armada/tasks/027-the-label-and-the-lens.md`.
**Learnings.** The finder was good; everything that was wrong was found by RUNNING it and
measuring the output, never by reading code.
- **96 kHz silently broke it.** librosa's hop/window defaults are sample-rate-relative, so
at 96 k every analysis frame spans half the musical time. MAREA declares `123BPM` in its
filenames = free ground truth: native gave 123/123/124/**119**/**128.1**/**128.5** bpm at
a 0.661 top score; resampled to 44.1 k first, 123.0–123.1 on every candidate at 0.859, in
a third of the time. Rate is a correctness input, not a performance knob.
- **Bar length must come from the TRACK, not the window.** Candidate bpm is a per-window
mean — right for drift, wrong for a bar length: ME's grid read 129.2 while both its loops
were cut at 150.5, so no two loops in a kit layered. Use the MODE of the local-BPM
histogram plus a confidence (share of beats within ±5 %). The 35 % gate landed in a clean
empty gap: every track ≥46 % produced good loops, the one at 20 % correctly produced none.
- **Chops beat loops mechanically, always.** `grade` scores a sub-0.75 s clip as a one-shot
on `0.5·level + 0.3·dc + 0.2·zc` — no seam, no bar term — and export already removes DC
and snaps zero crossings, so chops land near 1.0 while loops pay 0.35 on seam. Measured:
chops 0.993–0.999, loops 0.841–0.881, and the vocal kit came out as three stabs with no
loops. Never rank two modes against one rubric.
- **The periodicity check measured loop LENGTH, then ROLE.** Autocorrelation at lag *k* has
only *N−k* overlapping frames; normalised by `ac[0]` it scored a *perfectly periodic*
click track 0.418/0.623/0.754 at 2/4/8 bars. Divide by the per-lag overlap count → flat.
Then the floor had to go per family (drums 0.754, fx 0.549, tonal 0.214, bass 0.173, vox
0.087): one threshold condemned 12 of 20 vocal loops for being vocal.
- **Dead bars grade S.** A 4-bar loop ending in a fade is silent-to-silent (perfect seam) at
an exact bar length. 10 of the first 161 cuts. An objective defect with an objective test
belongs in the gate, not a report.
- **`amp = gain^4` means kits must be staged.** MAREA's ALL DRUMS and KIT both peak at
**+0.2 dBFS**, so `# gain 1.2` is +7.6 dB and the orbit clips on contact. −6 dBFS ceiling,
as ONE gain per kit (per-file would flatten a sparse hat against a full drum bus) and
never boosting (a quiet stem is quiet on purpose).
- **One rotation pass leaves 1 loop in 6 opening on a weak beat** — the "timed ok but cut
BCDA/DABC" complaint from 2026-07-11. The roll point is zc-snapped up to ~10 ms against a
20 ms attack window, enough to flip near-tied slots. Check-and-repeat: best slot 29 → 46
of 60, worse half 10 → 1.
- **Dead end: HPSS for percussiveness.** It hears a modern kick as harmonic and calls a drum
bus 9 % percussive. Also slow. Frame-RMS crest gating onset density is the cheap win — but
crest is a *sparsity* measure (a single vocal phrase reads 17.98), so it may only gate.
- **The checks caught two bugs in my own work**, which is the whole argument for them:
`kitgate` re-grades the shipped files rather than trusting the manifest, and found that
staging applied its gain AFTER grading, so the record described audio never written. And a
report section never rendered — a `str.replace` whose anchor didn't match, in the one patch
where I hadn't asserted on it.
- **Nearly filed a false alarm:** the first "do loops start on the downbeat" measurement said
17 % and looked like a broken feature. It scored 16 slots with no full-band term while the
rotator scores `bars×4` WITH one — the disagreement was my instrument's. Factored the
scorer out so the two cannot drift, re-measured: 48 % best-slot, median miss 0.032. The bug
was real *and* the first number was wrong.
- **The deepest one is about authority.** "Never infer role from the name" is not "the
measurement always wins." Measurement called `ALL DRUMS` a bass (a modern kick carries the
mix's energy: lf 0.86, centroid 216 Hz), rhythmic `KEYS2` drums (5.1 onsets/s), sparse
`CLAP` tonal. Presence and bass are decided by measurement; everything else keeps the
producer's label, because a human's statement about their own stem is evidence, not
inference. A label WE invented (an unrecognised token) loses to measurement. Disagreements
get reported, never silently resolved. Banked as `feedback_label_and_lens`.
**Deps.** Unblocked #21 (the auditioner SPA PLN asked for), #22 (same driver at the
acapellas + the 27 demucs catches), #23 (kitcheck/restage over the 163 existing kits).
Runs 1 and 2 were discarded — each predated a fix; run 3 is the deliverable.
#!/usr/bin/env python3
"""Register freshly cut third-party sample kits in the rights ledger, blocked by default.
A kit cut from somebody else's stems is not releasable per-track until a human says so
(`reference_sample_rights_audit`). Left alone, a new `fred_*` bank would land in the
ledger as `unknown` the next time `rights_audit.py --init` ran — which does block, but
blocks with no reason attached, so whoever reads it later has to re-derive where the
audio came from. Recording the provenance at the moment of cutting is the whole point
of `feedback_metadata_provenance`: value, source, locator, date.
python3 register_fred_rights.py <fred_kits.json> [--source "..."]
"""
from __future__ import annotations
import argparse
import json
from datetime import date
from pathlib import Path
HERE = Path(__file__).resolve().parent
LEDGER = HERE / "rights_ledger.json"
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("cuts_json", type=Path)
ap.add_argument("--source", default="Fred again.. producer stem pack (fan dropbox)")
ap.add_argument("--status", default="third_party",
choices=["third_party", "unknown", "cleared", "original"])
a = ap.parse_args()
cuts = json.loads(a.cuts_json.read_text())
led = json.loads(LEDGER.read_text())
banks = led.setdefault("banks", {})
counts: dict[str, int] = {}
tracks: dict[str, set] = {}
for c in cuts:
counts[c["kit"]] = counts.get(c["kit"], 0) + 1
tracks.setdefault(c["kit"], set()).add(c["track"])
added = updated = 0
for kit, n in sorted(counts.items()):
note = f"cut by engine.stempack from {a.source} — source track(s): " \
f"{', '.join(sorted(tracks[kit]))}"
if kit in banks and banks[kit].get("status") not in (None, "unknown"):
updated += 1 # never overwrite a human verdict
banks[kit]["files"] = n
continue
banks[kit] = {"status": a.status, "origin": "local", "files": n,
"note": note, "decided": date.today().isoformat()}
added += 1
LEDGER.write_text(json.dumps(led, indent=1) + "\n")
print(f"{added} bank(s) registered as {a.status}; {updated} already had a verdict and were left alone")
print(f"ledger: {LEDGER}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
......@@ -285,7 +285,7 @@
"status": "cleared",
"origin": "local",
"files": 582,
"note": "same source as jungle_breaks open-domain CD (PLN 2026-08-16)",
"note": "same source as jungle_breaks \u2014 open-domain CD (PLN 2026-08-16)",
"decided": "2026-08-16"
},
"perfect": {
......@@ -427,6 +427,307 @@
"files": 32,
"note": "",
"decided": null
},
"fred_angie_bass": {
"status": "third_party",
"origin": "local",
"files": 3,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE",
"decided": "2026-08-21"
},
"fred_angie_drums": {
"status": "third_party",
"origin": "local",
"files": 3,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE",
"decided": "2026-08-21"
},
"fred_angie_fx": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE",
"decided": "2026-08-21"
},
"fred_angie_hits": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE",
"decided": "2026-08-21"
},
"fred_angie_tonal": {
"status": "third_party",
"origin": "local",
"files": 5,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE",
"decided": "2026-08-21"
},
"fred_angie_vox": {
"status": "third_party",
"origin": "local",
"files": 7,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE",
"decided": "2026-08-21"
},
"fred_angieinterlude_bass": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE INTERLUDE",
"decided": "2026-08-21"
},
"fred_angieinterlude_fx": {
"status": "third_party",
"origin": "local",
"files": 4,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE INTERLUDE",
"decided": "2026-08-21"
},
"fred_angieinterlude_hits": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE INTERLUDE",
"decided": "2026-08-21"
},
"fred_angieinterlude_vox": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ANGIE INTERLUDE",
"decided": "2026-08-21"
},
"fred_bighen_bass": {
"status": "third_party",
"origin": "local",
"files": 3,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): BIG HEN",
"decided": "2026-08-21"
},
"fred_bighen_drums": {
"status": "third_party",
"origin": "local",
"files": 9,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): BIG HEN",
"decided": "2026-08-21"
},
"fred_bighen_fx": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): BIG HEN",
"decided": "2026-08-21"
},
"fred_bighen_hits": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): BIG HEN",
"decided": "2026-08-21"
},
"fred_bighen_tonal": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): BIG HEN",
"decided": "2026-08-21"
},
"fred_bighen_vox": {
"status": "third_party",
"origin": "local",
"files": 8,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): BIG HEN",
"decided": "2026-08-21"
},
"fred_dermot_bass": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): DERMOT",
"decided": "2026-08-21"
},
"fred_dermot_drums": {
"status": "third_party",
"origin": "local",
"files": 7,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): DERMOT",
"decided": "2026-08-21"
},
"fred_dermot_hits": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): DERMOT",
"decided": "2026-08-21"
},
"fred_dermot_tonal": {
"status": "third_party",
"origin": "local",
"files": 3,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): DERMOT",
"decided": "2026-08-21"
},
"fred_dermot_vox": {
"status": "third_party",
"origin": "local",
"files": 5,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): DERMOT",
"decided": "2026-08-21"
},
"fred_iamaparty_bass": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): IAMAPARTY",
"decided": "2026-08-21"
},
"fred_iamaparty_drums": {
"status": "third_party",
"origin": "local",
"files": 4,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): IAMAPARTY",
"decided": "2026-08-21"
},
"fred_iamaparty_fx": {
"status": "third_party",
"origin": "local",
"files": 4,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): IAMAPARTY",
"decided": "2026-08-21"
},
"fred_iamaparty_hits": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): IAMAPARTY",
"decided": "2026-08-21"
},
"fred_iamaparty_tonal": {
"status": "third_party",
"origin": "local",
"files": 3,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): IAMAPARTY",
"decided": "2026-08-21"
},
"fred_iamaparty_vox": {
"status": "third_party",
"origin": "local",
"files": 3,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): IAMAPARTY",
"decided": "2026-08-21"
},
"fred_marea_bass": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): MAREA",
"decided": "2026-08-21"
},
"fred_marea_drums": {
"status": "third_party",
"origin": "local",
"files": 12,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): MAREA",
"decided": "2026-08-21"
},
"fred_marea_hits": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): MAREA",
"decided": "2026-08-21"
},
"fred_marea_tonal": {
"status": "third_party",
"origin": "local",
"files": 5,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): MAREA",
"decided": "2026-08-21"
},
"fred_marea_vox": {
"status": "third_party",
"origin": "local",
"files": 8,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): MAREA",
"decided": "2026-08-21"
},
"fred_me_bass": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ME",
"decided": "2026-08-21"
},
"fred_me_fx": {
"status": "third_party",
"origin": "local",
"files": 4,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ME",
"decided": "2026-08-21"
},
"fred_me_tonal": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ME",
"decided": "2026-08-21"
},
"fred_me_vox": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): ME",
"decided": "2026-08-21"
},
"fred_pleasemakeitbetter_hits": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): PLEASEMAKEITBETTER",
"decided": "2026-08-21"
},
"fred_pleasemakeitbetter_vox": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): PLEASEMAKEITBETTER",
"decided": "2026-08-21"
},
"fred_yasminah_bass": {
"status": "third_party",
"origin": "local",
"files": 2,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): YASMINAH",
"decided": "2026-08-21"
},
"fred_yasminah_drums": {
"status": "third_party",
"origin": "local",
"files": 10,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): YASMINAH",
"decided": "2026-08-21"
},
"fred_yasminah_hits": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): YASMINAH",
"decided": "2026-08-21"
},
"fred_yasminah_tonal": {
"status": "third_party",
"origin": "local",
"files": 1,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): YASMINAH",
"decided": "2026-08-21"
},
"fred_yasminah_vox": {
"status": "third_party",
"origin": "local",
"files": 6,
"note": "cut by engine.stempack from Fred again.. producer stem pack (dropbox), /home/pln/Work/Sound/Samples/Fred/fred again stems \u2014 source track(s): YASMINAH",
"decided": "2026-08-21"
}
}
}
node_modules
dist
*.log
# Generated by tools/foundry/kitindex.py from the sample root — a 1.2 MB snapshot of
# audio that lives outside the repo. Regenerate, don't commit.
public/kits.json
# React + TypeScript + Vite
# armada/ui — the bridge
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
The instruments PLN actually looks at. One Vite project, several pages, one design
language (**the Ship's Bridge**`../PRODUCT.md` for the register, `../DESIGN.md` for
the visuals, managed with the `impeccable` skill from `armada/`).
Currently, two official plugins are available:
Stack: Vite + React 19 + TS + Tailwind 4 + shadcn conventions, Geist / Geist Mono.
Node ≥ 18 — `nvm use 22`.
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## The pages
## React Compiler
| URL | Name | What it answers |
|---|---|---|
| `/` | Take Judge | A/B two masters of the same take — which one ships |
| `/judge.html` | Set Judge | one pass down a whole gig, a verdict + comment per track → `decisions.json` |
| `/bounds.html` | Boundary Lab | where exactly does one track become the next |
| `/sextant.html` | Le Sextant | distribution cost/revenue simulation |
| `/kits.html` | **La Cale** | is this sample kit any good — audition, loop, rack up, export Tidal |
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
Each page is its own Rollup input in `vite.config.ts` and its own `src/<page>.tsx`
entry; shared bits live in `src/components/` and `src/lib/`.
## Expanding the ESLint configuration
## Running it
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
```bash
nvm use 22
npm ci
npm run dev # http://localhost:5173 — also on the LAN (server.host)
npm run build # tsc -b && vite build → dist/
npm test # vitest
npm run lint
```
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
For phone audition and anything that has to work outside dev, serve the build:
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```bash
python3 ../serve.py --dir dist --port 8792
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
`serve.py` is Range-capable, which stdlib `http.server` is not — without it, seeking a
90 MB master in `<audio>` does not work at all.
## Audio, and why it is mounted rather than bundled
The audio these pages play is gigabytes and lives outside the repo. `audio-mounts.json`
maps `/audio/<prefix>/...` to real directories, and it is read by **both**
`vite.config.ts` (dev) and `../serve.py` (built), so a URL that works in one works in
the other. A UI that only works under `npm run dev` is a demo, not a tool.
Longest prefix wins; `""` is the fallback mount. Add a prefix rather than a hardcoded
path — the masters move between gigs and the URLs should not.
## Data
Pages fetch generated JSON from `public/`. Some of it is committed (the gig-scoped
`judge-*.json`, `bounds-*.json`); `kits.json` is not — it is a 1.2 MB index of audio
that lives outside the repo, regenerated by
```bash
cd ../../tools/foundry && python3 kitindex.py --all
```
which writes `public/` **and** `dist/` if a build is present, because `npm run build`
copies `public/` exactly once and a page served from `dist/` would otherwise show an
index from before the last cut.
......@@ -7,7 +7,11 @@
"Masters live outside the repo (they are gigabytes). Mounting them by prefix",
"keeps the URLs stable while the paths move between gigs, and keeps `heads`",
"and `full` apart \u2014 `master split` names both '01 - Title.flac', so a single",
"flat mount would serve the 98 MB track when the UI asked for the 16 s head."
"flat mount would serve the 98 MB track when the UI asked for the 16 s head.",
"",
"'samples' is the whole custom sample root, not just one pack, so the kit",
"auditioner works on every kit PLN owns \u2014 the 163 already there and every",
"one the Foundry cuts next \u2014 without a config change per pack."
],
"mounts": {
"bounds": "/home/pln/Work/Sound/Prod/Opal26_master/bounds_v4",
......@@ -15,6 +19,7 @@
"full-club": "/home/pln/Work/Sound/Prod/Opal26_master/tracks_v4_club",
"heads": "/home/pln/Work/Sound/Prod/Opal26_master/heads_v4",
"joins": "/home/pln/Work/Sound/Prod/Opal26_master/joins_v4",
"samples": "/home/pln/Work/Sound/Samples",
"": "/home/pln/Work/Sound/Tidal/armada/tide-table/punkachien"
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>La Cale · L'Armada</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/kits.tsx"></script>
</body>
</html>
......@@ -32,7 +32,8 @@
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
"vite": "^8.0.12",
"vitest": "^4.1.11"
}
},
"node_modules/@babel/code-frame": {
......@@ -864,6 +865,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
......@@ -1147,6 +1155,24 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
......@@ -1467,6 +1493,119 @@
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
"integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.11",
"@vitest/utils": "4.1.11",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
"integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.11",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
"integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
"integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.11",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
"integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.11",
"@vitest/utils": "4.1.11",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
"integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
"integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.11",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@wavesurfer/react": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@wavesurfer/react/-/react-1.0.12.tgz",
......@@ -1517,6 +1656,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
......@@ -1608,6 +1757,16 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
......@@ -1702,6 +1861,13 @@
"node": ">=10.13.0"
}
},
"node_modules/es-module-lexer": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
......@@ -1897,6 +2063,16 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
......@@ -1907,6 +2083,16 @@
"node": ">=0.10.0"
}
},
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
......@@ -2575,6 +2761,20 @@
"node": ">=18"
}
},
"node_modules/obug": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT",
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
......@@ -2645,6 +2845,13 @@
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
......@@ -2855,6 +3062,13 @@
"node": ">=8"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
......@@ -2865,6 +3079,20 @@
"node": ">=0.10.0"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT"
},
"node_modules/std-env": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
"dev": true,
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
......@@ -2886,6 +3114,23 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
"integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
......@@ -2903,6 +3148,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyrainbow": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
"integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ts-api-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
......@@ -3101,6 +3356,96 @@
}
}
},
"node_modules/vitest": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
"integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.11",
"@vitest/mocker": "4.1.11",
"@vitest/pretty-format": "4.1.11",
"@vitest/runner": "4.1.11",
"@vitest/snapshot": "4.1.11",
"@vitest/spy": "4.1.11",
"@vitest/utils": "4.1.11",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^4.0.0-rc.1",
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.1.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.11",
"@vitest/browser-preview": "4.1.11",
"@vitest/browser-webdriverio": "4.1.11",
"@vitest/coverage-istanbul": "4.1.11",
"@vitest/coverage-v8": "4.1.11",
"@vitest/ui": "4.1.11",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"optional": true
},
"@vitest/coverage-istanbul": {
"optional": true
},
"@vitest/coverage-v8": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
},
"vite": {
"optional": false
}
}
},
"node_modules/wavesurfer.js": {
"version": "7.12.7",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.7.tgz",
......@@ -3123,6 +3468,23 @@
"node": ">= 8"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
......
......@@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@fontsource-variable/geist": "^5.2.9",
......@@ -34,6 +35,7 @@
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
"vite": "^8.0.12",
"vitest": "^4.1.11"
}
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource-variable/geist'
import '@fontsource-variable/geist-mono'
import './index.css'
import KitAuditioner from './kits/KitAuditioner.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<KitAuditioner />
</StrictMode>,
)
/**
* La Cale — where the cargo gets checked before it goes on stage.
*
* The Foundry cuts sample kits; nothing until now could tell you whether they were any
* good. A grade in a manifest is a claim, and the claim has been wrong in interesting
* ways (a fade-out graded S, and then 24 loops of pure silence graded A) — so the
* kit has to be *heard*, at tempo, against other loops, in a place where a bad one can
* be noticed in two seconds rather than mid-set.
*
* Three things, in the order you need them:
* 1. **browse** — every kit under the sample root, filtered by family, tier, mode
* 2. **audition** — one sample, untouched or looped (see `SampleRow`)
* 3. **rack** — several at once on one bar grid, exported as a `.tidal` block
*
* The index is generated by `tools/foundry/kitindex.py`; the header shows when, because
* a generated file that is never compared against its source is how the stale-preload
* crackle got to a venue.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Play, Square, Anchor, Search, RefreshCw } from 'lucide-react'
import { SampleRow } from './SampleRow'
import { Rack, type RackRow } from './Rack'
import { familyColor, fmtBpm, type Kit, type KitIndex, type Sample, type Tier } from './types'
import { Deck, PHRASE_BARS, CHOP_STEPS, audioCtx, audition, loadBuffer, type Audition, type Voice } from './engine'
/** Drop one key. `const { [id]: _unused, ...rest }` reads worse and lints worse. */
function without<T>(o: Record<string, T>, id: string): Record<string, T> {
const next = { ...o }
delete next[id]
return next
}
const FAMILIES = ['drums', 'hits', 'bass', 'tonal', 'vox', 'fx'] as const
const TIERS: Tier[] = ['S', 'A', 'B', 'C', 'D']
const KEY = 'pv_cale_rack'
/** `?kits=fred_*` narrows the rail on load, so a link can point at one pack. */
const kitGlob = new URLSearchParams(location.search).get('kits') ?? ''
const globMatch = (name: string, glob: string) =>
!glob || new RegExp('^' + glob.split('*').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$').test(name)
export default function KitAuditioner() {
const [idx, setIdx] = useState<KitIndex | null>(null)
const [err, setErr] = useState<string | null>(null)
const [kitSel, setKitSel] = useState<string | null>(null)
const [q, setQ] = useState('')
const [fam, setFam] = useState<Set<string>>(new Set())
const [minTier, setMinTier] = useState<Tier>('D')
const [mode, setMode] = useState<'all' | 'loop' | 'chop'>('all')
const [flaggedOnly, setFlaggedOnly] = useState(false)
const [focus, setFocus] = useState(0)
// audition state — one at a time, deliberately: two samples at once is what the rack is
const [live, setLive] = useState<{ id: string; mode: 'once' | 'loop' } | null>(null)
const auditionRef = useRef<Audition | null>(null)
const [head, setHead] = useState(0)
// the rack
const deckRef = useRef<Deck | null>(null)
// Hydrated at construction, not in an effect: the rack must never render empty for
// a frame and then fill in, or a play pressed early plays nothing.
const saved = useMemo(() => {
try { return JSON.parse(localStorage.getItem(KEY) || '{}') } catch { return {} }
}, [])
const [voices, setVoices] = useState<Record<string, Voice>>(saved.voices ?? {})
const [order, setOrder] = useState<string[]>(saved.order ?? [])
const [bpm, setBpm] = useState<number>(saved.bpm ?? 123)
const [playing, setPlaying] = useState(false)
const [pos, setPos] = useState(-1)
const [master, setMaster] = useState(0.8)
/** One action, so the focused row and the tempo can never lag the selection. */
const selectKit = useCallback((name: string, kitBpm?: number | null) => {
setKitSel(name)
setFocus(0)
// NOT rounded. Half-integer tempos are normal in this corpus (BIG HEN is 132.5),
// and 133 would stretch every loop in the kit by 0.4% — `loopAt` is exact, so the
// rounding error goes straight into the audio.
if (kitBpm) setBpm(kitBpm)
}, [])
useEffect(() => {
fetch('/kits.json')
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`${r.status} ${r.statusText} — run tools/foundry/kitindex.py`))))
.then((d: KitIndex) => {
setIdx(d)
const first = d.kits.find((k) => globMatch(k.kit, kitGlob)) ?? d.kits[0]
if (first) selectKit(first.kit, first.bpm)
})
.catch((e) => setErr(String(e.message ?? e)))
}, [selectKit])
// ── the kit rail, grouped by track ─────────────────────────────────────────
const kits = useMemo(
() => (idx?.kits ?? []).filter((k) => globMatch(k.kit, kitGlob)
&& (!q || k.kit.toLowerCase().includes(q.toLowerCase()))),
[idx, q],
)
const groups = useMemo(() => {
const g = new Map<string, Kit[]>()
for (const k of kits) g.set(k.track ?? '—', [...(g.get(k.track ?? '—') ?? []), k])
return [...g.entries()]
}, [kits])
const kit = useMemo(() => idx?.kits.find((k) => k.kit === kitSel) ?? null, [idx, kitSel])
const samples = useMemo(() => {
const floor = TIERS.indexOf(minTier)
return (kit?.samples ?? []).filter((s) => {
if (fam.size && !fam.has(s.family ?? '')) return false
if (s.tier && TIERS.indexOf(s.tier) > floor) return false
if (mode !== 'all' && (s.mode ?? (s.bars ? 'loop' : 'chop')) !== mode) return false
if (flaggedOnly && !s.flags.length) return false
return true
})
}, [kit, fam, minTier, mode, flaggedOnly])
// ── audition ───────────────────────────────────────────────────────────────
const stopAudition = useCallback(() => {
auditionRef.current?.stop()
auditionRef.current = null
setLive(null)
setHead(0)
}, [])
const play = useCallback((s: Sample, m: 'once' | 'loop') => {
stopAudition()
void loadBuffer(s.url).then((buf) => {
auditionRef.current = audition(buf, { loop: m === 'loop' })
setLive({ id: s.name, mode: m })
}).catch((e) => setErr(String(e)))
}, [stopAudition])
// playhead: read the clock, don't count frames — a dropped frame must not move it
useEffect(() => {
if (!live) return
let raf = 0
const step = () => {
const a = auditionRef.current
if (a) {
const el = audioCtx().currentTime - a.startedAt
if (live.mode === 'once' && el > a.durS) stopAudition()
else setHead((el % a.durS) / a.durS)
}
raf = requestAnimationFrame(step)
}
raf = requestAnimationFrame(step)
return () => cancelAnimationFrame(raf)
}, [live, stopAudition])
// ── the rack ───────────────────────────────────────────────────────────────
const deck = () => (deckRef.current ??= new Deck())
const addToRack = useCallback((s: Sample, kitName: string) => {
const id = `${kitName}/${s.name}`
setVoices((vs) => {
if (vs[id]) return without(vs, id)
const bars = s.bars ?? 0
return {
...vs,
[id]: {
id, url: s.url, bars, durS: s.dur_s ?? 0, gain: 1, muted: false,
// a new row arrives PLAYING, on every entry it has. Adding a loop and
// then having to find its cells before hearing anything is friction in
// the one interaction this page exists for.
cells: bars > 0
? Array.from({ length: PHRASE_BARS }, (_, i) => i % bars === 0)
: Array.from({ length: CHOP_STEPS }, (_, i) => i % 4 === 0),
},
}
})
setOrder((o) => (o.includes(id) ? o.filter((x) => x !== id) : [...o, id]))
}, [])
// Every voice change is pushed to the deck, buffers included. Decoding happens once
// per URL in `loadBuffer`, so re-running this on a fader move costs nothing.
useEffect(() => {
const d = deck()
d.bpm = bpm
const ids = new Set(Object.keys(voices))
for (const v of Object.values(voices)) void loadBuffer(v.url).then((b) => d.setVoice(v, b))
for (const id of order) if (!ids.has(id)) d.removeVoice(id)
}, [voices, bpm, order])
useEffect(() => { deck().setMaster(master) }, [master])
useEffect(() => {
if (!playing) return
const t = setInterval(() => setPos(deck().position()), 50)
return () => clearInterval(t)
}, [playing])
const togglePlay = useCallback(() => {
const d = deck()
if (d.playing) { d.stop(); setPlaying(false); setPos(-1) } else { d.start(); setPlaying(true) }
}, [])
// The rack survives a refresh. An arrangement you found by ear and then lost to a
// reload is the same mistake the set-judge already learned not to make.
useEffect(() => {
try { localStorage.setItem(KEY, JSON.stringify({ voices, order, bpm })) } catch { /* full or blocked */ }
}, [voices, order, bpm])
const rackRows: RackRow[] = useMemo(() => order.flatMap((id) => {
const v = voices[id]
if (!v) return []
const [kitName, name] = [id.slice(0, id.indexOf('/')), id.slice(id.indexOf('/') + 1)]
const s = idx?.kits.find((k) => k.kit === kitName)?.samples.find((x) => x.name === name)
return [{ voice: v, kit: kitName, n: s?.n ?? 0, family: s?.family ?? null, color: familyColor(s?.family) }]
}), [order, voices, idx])
// ── keys ───────────────────────────────────────────────────────────────────
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
const s = samples[focus]
if (e.key === 'j' || e.key === 'ArrowDown') { setFocus((f) => Math.min(f + 1, samples.length - 1)); e.preventDefault() }
else if (e.key === 'k' || e.key === 'ArrowUp') { setFocus((f) => Math.max(f - 1, 0)); e.preventDefault() }
else if (e.key === 'Enter' && s) play(s, 'once')
else if (e.key === 'l' && s) play(s, 'loop')
else if (e.key === 'a' && s && kit) addToRack(s, kit.kit)
else if (e.key === ' ') { togglePlay(); e.preventDefault() }
else if (e.key === 'Escape') stopAudition()
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [samples, focus, kit, play, addToRack, togglePlay, stopAudition])
if (err) return <Fail msg={err} />
if (!idx) return <div className="p-6 text-[13px] text-ink-faint">reading the index…</div>
const total = idx.kits.reduce((a, k) => a + k.n_samples, 0)
const flagged = kit?.samples.filter((s) => s.flags.length).length ?? 0
return (
<div className="h-screen flex flex-col bg-surface text-ink text-[13px]">
{/* ── bridge header: identity, then the transport ───────────────────── */}
<header className="flex items-center gap-3 px-3 py-2 border-b border-hairline shrink-0">
<Anchor size={15} className="text-magenta" />
<div>
<div className="font-semibold leading-tight">La Cale</div>
<div className="text-[11px] text-ink-faint leading-tight">
{idx.kits.length} kits · {total} samples · indexed {idx.generated.slice(0, 16).replace('T', ' ')}
</div>
</div>
<div className="flex-1" />
<button onClick={togglePlay}
className={`flex items-center gap-1.5 px-3 py-1 rounded-md text-[12px] font-medium
${playing ? 'bg-magenta text-surface' : 'bg-overlay text-ink hover:bg-overlay/70'}`}>
{playing ? <Square size={13} /> : <Play size={13} />}
{playing ? 'stop' : 'play rack'}
</button>
<label className="flex items-center gap-1.5 text-[11px] text-ink-faint">
bpm
<input type="number" min={40} max={220} step={0.25} value={bpm}
onChange={(e) => setBpm(Number(e.target.value) || 120)}
className="tnum w-20 bg-raised border border-hairline rounded-sm px-1.5 py-0.5 text-[12px] text-ink" />
</label>
{kit?.bpm && Math.abs(kit.bpm - bpm) > 0.01 && (
<button onClick={() => setBpm(kit.bpm!)}
className="flex items-center gap-1 text-[11px] text-wip hover:text-ink" title="match this kit's tempo">
<RefreshCw size={11} /> {fmtBpm(kit.bpm)}
</button>
)}
<label className="flex items-center gap-1.5 text-[11px] text-ink-faint" title="master">
out
<input type="range" min={0} max={1} step={0.01} value={master}
onChange={(e) => setMaster(Number(e.target.value))}
className="sx-range w-20" style={{ ['--fill' as string]: `${master * 100}%` }} />
</label>
<div className="text-[10px] text-ink-faint/70 font-mono pl-1">
j/k move · ⏎ once · l loop · a rack · ␣ play
</div>
</header>
<div className="flex flex-1 min-h-0">
{/* ── the rail ─────────────────────────────────────────────────────── */}
<aside className="w-60 shrink-0 border-r border-hairline flex flex-col">
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-hairline">
<Search size={12} className="text-ink-faint" />
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="filter kits"
className="flex-1 bg-transparent text-[12px] outline-none placeholder:text-ink-faint/60" />
</div>
<div className="flex-1 overflow-y-auto">
{groups.map(([track, ks]) => (
<div key={track}>
<div className="px-2 pt-2 pb-0.5 text-[10px] uppercase tracking-wide text-ink-faint/80">{track}</div>
{ks.map((k) => (
<button key={k.kit} onClick={() => selectKit(k.kit, k.bpm)}
className={`w-full flex items-center gap-2 px-2 py-1 text-left
${k.kit === kitSel ? 'bg-overlay' : 'hover:bg-raised'}`}>
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: familyColor(k.family) }} />
<span className="flex-1 truncate text-[12px]" title={k.kit}>
{k.family ?? k.kit}
</span>
<span className="tnum text-[11px] text-ink-faint">{k.n_samples}</span>
</button>
))}
</div>
))}
</div>
</aside>
{/* ── the kit ──────────────────────────────────────────────────────── */}
<main className="flex-1 min-w-0 flex flex-col">
<div className="flex items-center gap-2 px-2 py-1.5 border-b border-hairline shrink-0">
<div className="font-mono text-[12px] text-ink">{kit?.kit}</div>
<div className="flex gap-1">
{FAMILIES.filter((f) => kit?.samples.some((s) => s.family === f)).map((f) => (
<button key={f} onClick={() => setFam((p) => {
const n = new Set(p)
if (n.has(f)) n.delete(f); else n.add(f)
return n
})}
className={`px-1.5 py-0.5 text-[11px] rounded-sm border
${fam.has(f) ? 'border-transparent' : 'border-hairline text-ink-faint'}`}
style={fam.has(f) ? { background: familyColor(f), color: 'var(--color-surface)' } : undefined}>
{f}
</button>
))}
</div>
<div className="flex-1" />
<select value={mode} onChange={(e) => setMode(e.target.value as typeof mode)}
className="bg-raised border border-hairline rounded-sm px-1 py-0.5 text-[11px]">
<option value="all">loops + chops</option>
<option value="loop">loops</option>
<option value="chop">chops</option>
</select>
<select value={minTier} onChange={(e) => setMinTier(e.target.value as Tier)}
className="bg-raised border border-hairline rounded-sm px-1 py-0.5 text-[11px]">
{TIERS.map((t) => <option key={t} value={t}>{t}+</option>)}
</select>
{flagged > 0 && (
<button onClick={() => setFlaggedOnly((v) => !v)}
className={`px-1.5 py-0.5 text-[11px] rounded-sm border
${flaggedOnly ? 'border-wip text-wip' : 'border-hairline text-ink-faint'}`}
title="samples the grader flagged — worth a second listen">
! {flagged}
</button>
)}
<div className="tnum text-[11px] text-ink-faint w-14 text-right">{samples.length} shown</div>
</div>
<div className="flex-1 overflow-y-auto">
{samples.map((s, i) => (
<SampleRow key={s.name} s={s} kit={kit!.kit}
playing={live?.id === s.name ? live.mode : null}
head={live?.id === s.name ? head : null}
inRack={!!voices[`${kit!.kit}/${s.name}`]}
focused={i === focus}
onFocus={() => setFocus(i)}
onPlay={(m) => play(s, m)}
onStop={stopAudition}
onRack={() => addToRack(s, kit!.kit)} />
))}
{!samples.length && <div className="p-4 text-[12px] text-ink-faint">nothing matches these filters.</div>}
</div>
</main>
</div>
{/* ── the rack ───────────────────────────────────────────────────────── */}
<section className="h-[17rem] shrink-0 border-t border-hairline flex flex-col bg-raised/40">
<div className="flex items-center gap-2 px-2 py-1 border-b border-hairline shrink-0">
<div className="text-[11px] uppercase tracking-wide text-ink-faint">rack</div>
<div className="text-[11px] text-ink-faint">
{rackRows.length} voice{rackRows.length === 1 ? '' : 's'} · {PHRASE_BARS}-bar phrase @ {fmtBpm(bpm)} bpm
</div>
{playing && pos >= 0 && (
<div className="tnum font-mono text-[11px] text-magenta">bar {pos + 1}/{PHRASE_BARS}</div>
)}
</div>
<Rack rows={rackRows} bpm={bpm} playing={playing} position={pos}
onChange={(id, patch) => setVoices((vs) => ({ ...vs, [id]: { ...vs[id], ...patch } }))}
onRemove={(id) => { setVoices((vs) => without(vs, id)); setOrder((o) => o.filter((x) => x !== id)); deck().removeVoice(id) }}
onClear={() => { setVoices({}); setOrder([]); deck().clear() }} />
</section>
</div>
)
}
function Fail({ msg }: { msg: string }) {
return (
<div className="p-6 max-w-xl">
<div className="text-blocked text-[13px] mb-2">{msg}</div>
<pre className="text-[11px] text-ink-faint font-mono">
{`cd tools/foundry
python3 kitindex.py --cuts packs/fred/fred_kits.json --all`}
</pre>
</div>
)
}
/**
* The rack — click cells, hear a groove, paste it into a `.tidal` file.
*
* A kit is not validated one sample at a time. The questions that decide whether a cut
* was worth making are "do these two loops sit together", "does this vocal survive on
* top of that break", "is this bass the same tempo as it claims" — and none of them can
* be answered in a file browser. So the rack is a small step sequencer whose grid means
* exactly what Tidal means:
*
* - A **bar-loop** row has one cell per bar of the phrase, but only on multiples of its
* own length: a 4-bar loop in an 8-bar phrase has two entries, not eight, because
* starting it on bar 3 would play its bar 1 under the phrase's bar 4. The cells
* translate to `mask "t f"`.
* - A **chop** row has 16 steps in one bar, repeated. The cells translate to the
* structure string `s "x ~ ~ x …"`.
*
* Everything plays at the deck's tempo via `loopAt`, so the layering you hear is the
* layering you get, and the export is not an approximation of the session — it is the
* same two numbers written in Tidal's syntax.
*/
import { useEffect, useState } from 'react'
import { Trash2, Copy, Volume2, VolumeX, Check } from 'lucide-react'
import { CHOP_STEPS, PHRASE_BARS, type Voice } from './engine'
import { familyColor } from './types'
import { toTidal, toStrudel, type ExportRow } from './tidal'
export type RackRow = ExportRow & { color: string }
type Props = {
rows: RackRow[]
bpm: number
playing: boolean
position: number // bar index within the phrase, -1 when stopped
onChange: (id: string, patch: Partial<Voice>) => void
onRemove: (id: string) => void
onClear: () => void
}
export function Rack({ rows, bpm, playing, position, onChange, onRemove, onClear }: Props) {
const [tab, setTab] = useState<'tidal' | 'strudel'>('tidal')
const [copied, setCopied] = useState(false)
useEffect(() => { if (copied) { const t = setTimeout(() => setCopied(false), 1200); return () => clearTimeout(t) } }, [copied])
const code = tab === 'tidal' ? toTidal(rows, bpm) : toStrudel(rows, bpm)
if (!rows.length) {
return (
<div className="p-4 text-[12px] text-ink-faint max-w-2xl">
The rack is empty. Add samples with <span className="font-mono">+</span> — or press
<span className="font-mono"> a </span>on a focused row — then hit play. Loops are
stretched to the deck tempo with <span className="font-mono">loopAt</span>, which is
what Tidal will do, so what you hear here is what the rig plays.
</div>
)
}
return (
<div className="flex min-h-0 flex-1">
<div className="flex-1 overflow-y-auto">
{rows.map(({ voice: v, kit, n, family }) => {
const isChop = v.bars === 0
const cells = isChop ? CHOP_STEPS : PHRASE_BARS
const stride = isChop ? 1 : v.bars
const col = familyColor(family)
return (
<div key={v.id} className="flex items-center gap-2 px-2 py-1 border-b border-hairline/60">
<button onClick={() => onChange(v.id, { muted: !v.muted })}
className={`p-1 rounded-sm hover:bg-overlay ${v.muted ? 'text-ink-faint' : 'text-ink-muted'}`}
title={v.muted ? 'unmute' : 'mute'}>
{v.muted ? <VolumeX size={13} /> : <Volume2 size={13} />}
</button>
<div className="w-40 min-w-0">
<div className="truncate text-[12px] leading-tight" style={{ color: v.muted ? 'var(--color-ink-faint)' : col }}>
{v.id.split('/')[1]}
</div>
<div className="truncate text-[10px] text-ink-faint leading-tight font-mono">
{kit} · n {n} · {isChop ? 'chop' : `loopAt ${v.bars}`}
</div>
</div>
{/* the grid. A cell is a bar (or a 16th, for a chop) of the phrase. */}
<div className="flex flex-1 gap-[3px]">
{Array.from({ length: cells }, (_, i) => {
const on = !!v.cells[i]
const usable = i % stride === 0
// where the playhead is, in this row's own units
const live = playing && (isChop
? false // a 16th playhead would strobe; the bar rule is enough
: position >= 0 && Math.floor(position / stride) * stride === i)
if (!usable) return <div key={i} className="flex-1" />
return (
<button key={i}
onClick={() => {
const next = [...v.cells]
next[i] = !on
onChange(v.id, { cells: next })
}}
style={{
flex: stride,
background: on ? col : 'var(--color-raised)',
opacity: v.muted ? 0.35 : 1,
boxShadow: live ? 'inset 0 0 0 1.5px var(--color-ink)' : undefined,
}}
className="h-6 rounded-sm border border-hairline/60 hover:brightness-125" />
)
})}
</div>
<input type="range" min={0} max={1.5} step={0.01} value={v.gain}
onChange={(e) => onChange(v.id, { gain: Number(e.target.value) })}
className="sx-range w-20" style={{ ['--fill' as string]: `${(v.gain / 1.5) * 100}%` }}
title={`fader ${v.gain.toFixed(2)} (linear)`} />
<button onClick={() => onRemove(v.id)} className="p-1 rounded-sm text-ink-faint hover:bg-overlay hover:text-blocked">
<Trash2 size={13} />
</button>
</div>
)
})}
</div>
{/* the payoff: the same groove as source you can paste into a live file */}
<div className="w-[27rem] shrink-0 border-l border-hairline flex flex-col">
<div className="flex items-center gap-1 px-2 py-1 border-b border-hairline">
{(['tidal', 'strudel'] as const).map((t) => (
<button key={t} onClick={() => setTab(t)}
className={`px-2 py-0.5 text-[11px] rounded-sm font-mono
${tab === t ? 'bg-overlay text-ink' : 'text-ink-faint hover:text-ink-muted'}`}>
{t}
</button>
))}
<div className="flex-1" />
<button onClick={() => { void navigator.clipboard.writeText(code); setCopied(true) }}
className="flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-sm text-ink-muted hover:bg-overlay">
{copied ? <Check size={12} className="text-ready" /> : <Copy size={12} />}
{copied ? 'copied' : 'copy'}
</button>
<button onClick={onClear} className="px-2 py-0.5 text-[11px] rounded-sm text-ink-faint hover:bg-overlay hover:text-blocked">
clear
</button>
</div>
<pre className="flex-1 overflow-auto p-2 text-[11px] leading-[1.45] font-mono text-ink-muted whitespace-pre">
{code}
</pre>
</div>
</div>
)
}
/**
* One sample: what it is, what it sounds like, and two ways to hear it.
*
* The two buttons are not the same test and the difference is the point of the page.
* **Once** plays the file untouched, which is how you hear its attack and its tail.
* **Loop** loops it seamlessly at rate 1, which is the only way to hear the seam the
* cutter actually produced — and the seam is the single thing that decides whether a
* loop is usable, because it is the sound the audience hears every bar.
*
* **+** puts it in the rack, where it stops being a file and becomes a voice in a
* groove at the deck's tempo.
*/
import { Play, Repeat, Square, Plus, Check } from 'lucide-react'
import { Waveform } from './Waveform'
import { familyColor, fmtBpm, TIER_COLOR, type Sample } from './types'
import { barErrorMs } from './engine'
type Props = {
s: Sample
kit: string
playing: 'once' | 'loop' | null
head: number | null
inRack: boolean
focused: boolean
onPlay: (mode: 'once' | 'loop') => void
onStop: () => void
onRack: () => void
onFocus: () => void
}
const TAG_AXES = ['instrument', 'texture', 'mood'] as const
/** Where in the source track, so two loops off the same stem are tellable apart. */
const mmss = (s: number) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`
export function SampleRow({ s, playing, head, inRack, focused, onPlay, onStop, onRack, onFocus }: Props) {
const col = familyColor(s.family)
const bars = s.bars ?? 0
const err = s.bars && s.bpm && s.dur_s ? barErrorMs(s.dur_s, s.bars, s.bpm) : 0
// top tag per axis, at all — a wall of CLAP labels is not information
const tags = TAG_AXES.map((a) => s.tags?.[a]?.[0]).filter(Boolean) as [string, number][]
return (
<div
onMouseDown={onFocus}
className={`grid grid-cols-[2.2rem_14rem_3.2rem_1fr_5.5rem_4.6rem] items-center gap-2
px-2 py-1 border-b border-hairline/60 cursor-default
${focused ? 'bg-overlay' : 'hover:bg-raised'}`}
>
{/* the index you type. `# n 3` is the whole reason the kit is addressable */}
<div className="tnum font-mono text-[13px] text-ink-muted text-right pr-1">{s.n}</div>
<div className="min-w-0" title={s.file}>
<div className="truncate text-[13px] leading-tight" style={{ color: col }}>
{s.stem_role ?? s.name}
</div>
<div className="truncate text-[11px] text-ink-faint leading-tight">
{bars > 0
? <>{bars} bar{bars > 1 ? 's' : ''} · {s.bpm != null ? fmtBpm(s.bpm) : '?'} bpm
{s.start_s != null && <> · @{mmss(s.start_s)}</>}
{Math.abs(err) > 1 && <span className="text-wip"> · {err > 0 ? '+' : ''}{err.toFixed(0)}ms off</span>}</>
: <>{s.dur_s ? `${s.dur_s.toFixed(2)}s chop` : `${s.ch}ch ${(s.sr / 1000).toFixed(1)}k`}
{s.start_s != null && <> · @{mmss(s.start_s)}</>}</>}
</div>
</div>
<div className="flex items-center gap-1">
{s.tier && (
<span className="tnum font-mono text-[11px] px-1 rounded-sm"
style={{ color: TIER_COLOR[s.tier], border: `1px solid ${TIER_COLOR[s.tier]}55` }}
title={s.grade ? `grade ${s.grade.toFixed(3)}` : undefined}>
{s.tier}
</span>
)}
{s.flags.length > 0 && (
<span className="text-[11px] text-wip" title={s.flags.join(', ')}>!</span>
)}
</div>
{/* Tags UNDER the waveform, not over it. Absolutely positioned they covered the
first ~15% of every envelope — which is the downbeat, the one part of a loop
you actually need to see. */}
<div className="min-w-0">
<Waveform env={s.env} color={col} head={head} bars={bars} height={26} />
<div className="flex gap-1.5 h-[11px] overflow-hidden">
{tags.map(([t, p]) => (
<span key={t} className="text-[10px] leading-[11px] text-ink-faint/70 truncate"
title={`${t} (${(p * 100).toFixed(0)}%)`}>{t}</span>
))}
</div>
</div>
<div className="tnum font-mono text-[11px] text-ink-faint text-right">
{s.peak_dbfs.toFixed(1)} / {s.rms_dbfs.toFixed(0)}
</div>
<div className="flex items-center justify-end gap-0.5">
<button title="play once, untouched"
onClick={() => (playing === 'once' ? onStop() : onPlay('once'))}
className={`p-1 rounded-sm hover:bg-overlay ${playing === 'once' ? 'text-magenta' : 'text-ink-muted'}`}>
{playing === 'once' ? <Square size={13} /> : <Play size={13} />}
</button>
<button title="loop it — the only way to hear the seam"
onClick={() => (playing === 'loop' ? onStop() : onPlay('loop'))}
className={`p-1 rounded-sm hover:bg-overlay ${playing === 'loop' ? 'text-magenta' : 'text-ink-muted'}`}>
<Repeat size={13} />
</button>
<button title={inRack ? 'in the rack' : 'add to the rack'} onClick={onRack}
className={`p-1 rounded-sm hover:bg-overlay ${inRack ? 'text-ready' : 'text-ink-muted'}`}>
{inRack ? <Check size={13} /> : <Plus size={13} />}
</button>
</div>
</div>
)
}
/**
* The envelope of a sample, drawn from the 128 numbers `kitindex.py` precomputed.
*
* Deliberately not wavesurfer. A kit page shows a hundred-odd rows; a hundred
* wavesurfer instances means a hundred fetches and a hundred decodes to draw
* something 200 px wide, so the list would take a minute to appear and the audio
* thread would be fighting the render. The index already carries the envelope, so a
* row costs one <svg> and no network at all — audio is fetched only when a sample is
* actually played. (Same instinct as the tide-table's ship-the-index rule.)
*
* The playhead is a CSS transform driven by the deck's own clock, so it cannot drift
* away from what you are hearing even when React is busy.
*/
import { memo } from 'react'
type Props = {
env: number[]
color: string
/** 0..1, or null when this sample is not sounding */
head?: number | null
/** bar boundaries to rule, as fractions of the width */
bars?: number
height?: number
className?: string
}
export const Waveform = memo(function Waveform(
{ env, color, head = null, bars = 0, height = 34, className }: Props,
) {
const n = env.length || 1
const mid = height / 2
// one polygon, mirrored: cheaper than 128 rects and it reads as a waveform rather
// than as a bar chart, which matters when you are scanning for where the hit is
const top = env.map((v, i) => `${(i / n) * 100},${mid - (v / 100) * (mid - 1)}`)
const bot = env.map((v, i) => `${(i / n) * 100},${mid + (v / 100) * (mid - 1)}`).reverse()
return (
<svg className={className} viewBox={`0 0 100 ${height}`} height={height}
preserveAspectRatio="none" style={{ width: '100%', display: 'block' }}>
<polygon points={[...top, ...bot].join(' ')} fill={color} fillOpacity={0.75} />
{bars > 1 && Array.from({ length: bars - 1 }, (_, i) => (
<line key={i} x1={((i + 1) / bars) * 100} x2={((i + 1) / bars) * 100}
y1={0} y2={height} stroke="var(--hairline)" strokeWidth={0.3} />
))}
{head != null && (
<line x1={head * 100} x2={head * 100} y1={0} y2={height}
stroke="var(--ink)" strokeWidth={0.5} />
)}
</svg>
)
})
/**
* The scheduler's one musical rule, tested away from the audio graph.
*
* `hitsInBar` decides what plays and when. Get it wrong and the rack still makes
* sound — it just makes the wrong sound, with a groove that turns around in the wrong
* place, which is exactly the class of bug you discover mid-set.
*/
import { describe, expect, it } from 'vitest'
import { hitsInBar, barSeconds, CHOP_STEPS, PHRASE_BARS, type Voice } from '../engine'
const loop = (o: Partial<Voice> = {}): Voice => ({
id: 'k/x', url: '/x.wav', bars: 4, durS: 7.805, gain: 1, muted: false,
cells: Array.from({ length: PHRASE_BARS }, () => true),
...o,
})
describe('hitsInBar — bar loops', () => {
it('starts a 4-bar loop only on bars 0 and 4 of an 8-bar phrase', () => {
const v = loop({ bars: 4 })
const fires = Array.from({ length: PHRASE_BARS }, (_, b) => hitsInBar(v, b, 123).length)
expect(fires).toEqual([1, 0, 0, 0, 1, 0, 0, 0])
})
it('starts a 2-bar loop on every even bar, and a 1-bar loop on every bar', () => {
expect(Array.from({ length: 8 }, (_, b) => hitsInBar(loop({ bars: 2 }), b, 123).length))
.toEqual([1, 0, 1, 0, 1, 0, 1, 0])
expect(Array.from({ length: 8 }, (_, b) => hitsInBar(loop({ bars: 1 }), b, 123).length))
.toEqual([1, 1, 1, 1, 1, 1, 1, 1])
})
it('holds the voice for exactly its own bar count', () => {
const [h] = hitsInBar(loop({ bars: 4 }), 0, 123)
expect(h.holdS).toBeCloseTo(4 * barSeconds(123), 6)
expect(h.offsetS).toBe(0)
})
it('respects a cleared cell at the entry it would otherwise use', () => {
const cells = Array.from({ length: PHRASE_BARS }, () => true)
cells[4] = false
expect(hitsInBar(loop({ bars: 4, cells }), 0, 123)).toHaveLength(1)
expect(hitsInBar(loop({ bars: 4, cells }), 4, 123)).toHaveLength(0)
})
it('keeps the phrase when the transport runs past it, and when it runs backwards', () => {
// bar 8 is bar 0 of the next phrase; a negative index must not fall off the array
expect(hitsInBar(loop({ bars: 4 }), 8, 123)).toHaveLength(1)
expect(hitsInBar(loop({ bars: 4 }), 12, 123)).toHaveLength(1)
expect(() => hitsInBar(loop({ bars: 4 }), -3, 123)).not.toThrow()
})
it('plays nothing when muted or at zero gain', () => {
expect(hitsInBar(loop({ muted: true }), 0, 123)).toHaveLength(0)
expect(hitsInBar(loop({ gain: 0 }), 0, 123)).toHaveLength(0)
})
it('stretches to the DECK tempo, not the file\'s — that is what layering means', () => {
const v = loop({ bars: 4, durS: 7.805 }) // 4 bars at 123 bpm
expect(hitsInBar(v, 0, 123)[0].rate).toBeCloseTo(1, 3)
expect(hitsInBar(v, 0, 150)[0].rate).toBeCloseTo(150 / 123, 3)
})
})
describe('hitsInBar — chops', () => {
const chop = (cells: boolean[]): Voice =>
({ ...loop(), bars: 0, durS: 0.4, cells })
it('fires on every armed 16th of every bar', () => {
const cells = Array.from({ length: CHOP_STEPS }, (_, i) => i % 4 === 0)
const hits = hitsInBar(chop(cells), 0, 120)
expect(hits).toHaveLength(4)
const step = barSeconds(120) / CHOP_STEPS
expect(hits.map((h) => h.offsetS / step)).toEqual([0, 4, 8, 12])
// unlike a bar-loop, a chop is not constrained to a stride — bar 3 plays too
expect(hitsInBar(chop(cells), 3, 120)).toHaveLength(4)
})
it('never stretches a chop: a stab is a stab at any tempo', () => {
const cells = Array.from({ length: CHOP_STEPS }, (_, i) => i === 0)
expect(hitsInBar(chop(cells), 0, 90)[0].rate).toBe(1)
expect(hitsInBar(chop(cells), 0, 174)[0].rate).toBe(1)
})
it('pushes odd 16ths late by the swing fraction, and leaves even ones alone', () => {
const cells = Array.from({ length: CHOP_STEPS }, () => true)
const step = barSeconds(120) / CHOP_STEPS
const hits = hitsInBar(chop(cells), 0, 120, 0.25)
expect(hits[0].offsetS).toBeCloseTo(0, 9)
expect(hits[1].offsetS).toBeCloseTo(step * 1.25, 9)
expect(hits[2].offsetS).toBeCloseTo(step * 2, 9)
})
it('never holds a chop past the end of its buffer', () => {
const cells = Array.from({ length: CHOP_STEPS }, (_, i) => i === 0)
expect(hitsInBar(chop(cells), 0, 120, 0, 0.12)[0].holdS).toBe(0.12)
})
})
/**
* The export is the whole payoff, so it gets the tests.
*
* Everything else on this page fails loudly — a broken fetch shows an error, a bad
* decode throws. The export fails *silently*: it produces plausible Tidal that plays
* the wrong sample, at the wrong length, on an orbit that is already in use, and you
* find out on stage. So the rules that are easy to get backwards get pinned here.
*/
import { describe, expect, it } from 'vitest'
import { maskOf, stepsOf, toTidal, toStrudel, FAMILY_ORBIT } from '../tidal'
import { loopAtRate, barErrorMs, PHRASE_BARS, CHOP_STEPS, type Voice } from '../engine'
const voice = (o: Partial<Voice> = {}): Voice => ({
id: 'fred_marea_drums/03_kit_4b', url: '/x.wav', bars: 4, durS: 7.805,
gain: 1, muted: false,
cells: Array.from({ length: PHRASE_BARS }, (_, i) => i % 4 === 0),
...o,
})
describe('loopAt', () => {
it('is the ratio the deck plays and the number Tidal is told', () => {
// 4 bars at 123 bpm is 7.8049 s; a file already that long plays at rate 1
expect(loopAtRate(7.8049, 4, 123)).toBeCloseTo(1, 3)
// the same file asked to be 4 bars of 150 bpm has to run faster
expect(loopAtRate(7.8049, 4, 150)).toBeCloseTo(150 / 123, 3)
})
it('never divides by zero on a chop', () => {
expect(loopAtRate(0.4, 0, 123)).toBe(1)
})
it('reports how far off a whole bar a file is, in ms', () => {
expect(barErrorMs(7.8049, 4, 123)).toBeCloseTo(0, 1)
expect(barErrorMs(7.9049, 4, 123)).toBeCloseTo(100, 0)
})
})
describe('mask', () => {
it('is omitted when every entry is on — an all-t mask is noise in the source', () => {
expect(maskOf([true, false, false, false, true, false, false, false], 4, 8)).toBeNull()
})
it('samples the grid on the loop\'s own stride, not every bar', () => {
// a 4-bar loop in an 8-bar phrase has TWO entries. Reading all eight cells would
// emit `mask "t f f f f f f f"` and silence the second entry that is actually on.
const cells = [true, false, false, false, false, false, false, false]
expect(maskOf(cells, 4, 8)).toBe('t f')
expect(maskOf(cells, 1, 8)).toBe('t f f f f f f f')
})
it('renders a chop row as a structure string of the right length', () => {
const c = Array.from({ length: CHOP_STEPS }, (_, i) => i % 4 === 0)
expect(stepsOf(c).split(' ')).toHaveLength(CHOP_STEPS)
expect(stepsOf(c)).toBe('x ~ ~ ~ x ~ ~ ~ x ~ ~ ~ x ~ ~ ~')
})
})
describe('toTidal', () => {
const row = (o: Partial<Voice> = {}, family = 'drums', n = 3) =>
({ voice: voice(o), kit: 'fred_marea_drums', n, family })
it('emits the index and the bar count together, because both must be right', () => {
const out = toTidal([row()], 123)
expect(out).toContain('loopAt 4 $ s "fred_marea_drums" # n 3')
expect(out).toContain('setcps (0.5125)') // 123 bpm / 60 / 4
})
it('converts the linear fader to Tidal\'s quartic gain', () => {
// amp = gain^4, so a fader at half amplitude is gain 0.5^(1/4) = 0.84, not 0.5.
// Copying the fader straight across would be a 12 dB error.
expect(toTidal([row({ gain: 0.5 })], 123)).toContain('# gain 0.84')
// and unity is left off entirely rather than written as `# gain 1.00`
expect(toTidal([row({ gain: 1 })], 123)).not.toContain('# gain')
})
it('puts each family on its own orbit and never doubles up', () => {
const rows = [row({ id: 'a/1' }, 'drums', 0), row({ id: 'b/2' }, 'drums', 1)]
const lines = toTidal(rows, 123).split('\n').filter((l) => l.startsWith('d'))
const orbits = lines.map((l) => l.slice(0, 2))
expect(new Set(orbits).size).toBe(2) // two voices, two orbits
expect(orbits[0]).toBe(`d${FAMILY_ORBIT.drums}`)
})
it('skips muted rows and always ends on hush', () => {
const out = toTidal([row({ muted: true })], 123)
expect(out).not.toContain('loopAt')
expect(out.trimEnd().endsWith('hush')).toBe(true)
})
it('emits a chop as a structure, not as a loopAt', () => {
const out = toTidal([row({ bars: 0, cells: [true, false, false, false] })], 123)
expect(out).toContain('# n 3')
expect(out).not.toContain('loopAt')
})
})
describe('toStrudel', () => {
it('is silence when nothing is armed, never an empty stack', () => {
expect(toStrudel([], 123)).toBe('silence')
expect(toStrudel([{ voice: voice({ muted: true }), kit: 'k', n: 0, family: 'drums' }], 123))
.toBe('silence')
})
it('carries the same kit, index and bar count as the Tidal form', () => {
const out = toStrudel([{ voice: voice(), kit: 'fred_marea_drums', n: 3, family: 'drums' }], 123)
expect(out).toContain('s("fred_marea_drums").n(3).loopAt(4)')
})
})
/**
* The deck — a Web Audio transport that loops sample kits the way Tidal will.
*
* The point of this file is that auditioning a loop in a normal audio player tells
* you almost nothing about whether it is a good loop. A player stops at the end, so
* you never hear the seam; it plays at the file's own rate, so you never hear it
* against another loop; and it has no bar grid, so you never find out that the
* "4-bar" loop is 4.02 bars and walks away from the beat after a minute.
*
* So the rack schedules every voice explicitly on one grid, and sets
*
* rate = buffer.duration / (bars * 4 * 60 / bpm)
*
* which is exactly `loopAt bars` in Tidal: squeeze the file into that many bars of
* the current tempo. What you hear here is what `d1 $ loopAt 4 $ s "kit" # n 3`
* will do on the rig — the SPA is a test of the kit, not a preview of it.
*
* Two playback paths on purpose:
*
* - `audition` uses a single looping AudioBufferSourceNode at rate 1. One node, so
* drift cannot exist, and the file is heard UNTOUCHED — which is the only way to
* judge the seam the cut actually produced.
* - `Deck` schedules one node per bar per voice from a look-ahead clock. Zero drift
* by construction even when a loop is a millisecond off a bar, because every
* iteration is placed against the grid rather than against the previous iteration.
* That millisecond is real: the cutter's tolerance is 1 ms, which is 60 ms of skew
* after a minute if you let loops free-run.
*/
export type Voice = {
id: string // stable key: `${kit}/${name}`
url: string
bars: number // 0 = a chop: sub-bar, fired on the 16th grid, never stretched
durS: number
gain: number // 0..1.5, linear (this is a mixer fader, not Tidal's gain^4)
muted: boolean
cells: boolean[] // bar-loops: one per bar of the phrase. chops: 16 per bar.
}
export const PHRASE_BARS = 8
export const CHOP_STEPS = 16
const ctxRef: { ctx?: AudioContext } = {}
export function audioCtx(): AudioContext {
if (!ctxRef.ctx) ctxRef.ctx = new AudioContext({ latencyHint: 'playback' })
if (ctxRef.ctx.state === 'suspended') void ctxRef.ctx.resume()
return ctxRef.ctx
}
const buffers = new Map<string, Promise<AudioBuffer>>()
/** Decode once per URL. The rack re-triggers the same file every bar; re-fetching it
* would put a network request on the audio path. */
export function loadBuffer(url: string): Promise<AudioBuffer> {
let p = buffers.get(url)
if (!p) {
p = fetch(url)
.then((r) => (r.ok ? r.arrayBuffer() : Promise.reject(new Error(`${r.status} ${url}`))))
.then((b) => audioCtx().decodeAudioData(b))
buffers.set(url, p)
p.catch(() => buffers.delete(url)) // a failed decode must not be cached as a result
}
return p
}
export const isLoaded = (url: string) => buffers.has(url)
export const barSeconds = (bpm: number) => (4 * 60) / bpm
/** `loopAt bars` — the rate that makes this file exactly `bars` long at `bpm`. */
export function loopAtRate(durS: number, bars: number, bpm: number): number {
if (!bars || durS <= 0) return 1
return durS / (bars * barSeconds(bpm))
}
/** How far off a whole bar the file is, in ms, at its own declared tempo. The number
* the cutter promises is under 1 ms; showing it is how you notice when it isn't. */
export function barErrorMs(durS: number, bars: number, bpm: number): number {
if (!bars || !bpm) return 0
const want = bars * barSeconds(bpm)
return (durS - want) * 1000
}
// ── auditioning one sample, untouched ────────────────────────────────────────
export type Audition = { stop: () => void; startedAt: number; durS: number }
export function audition(buf: AudioBuffer, opts: { loop: boolean; gain?: number }): Audition {
const ctx = audioCtx()
const src = ctx.createBufferSource()
src.buffer = buf
src.loop = opts.loop
const g = ctx.createGain()
g.gain.value = opts.gain ?? 1
src.connect(g).connect(ctx.destination)
const t0 = ctx.currentTime
src.start(t0)
return {
startedAt: t0,
durS: buf.duration,
stop: () => { try { src.stop() } catch { /* already ended */ } },
}
}
export type Hit = { offsetS: number; rate: number; holdS: number }
/**
* What this voice plays during bar `barIndex`, as offsets from the top of that bar.
*
* Extracted from the scheduler because it is the only part with a musical rule in it,
* and a rule you cannot test is a rule you find out about on stage. The rule: a
* bar-loop only starts on a bar that is a multiple of ITS OWN length. A 4-bar loop in
* an 8-bar phrase therefore has two possible entries, not eight — firing it on bar 3
* would play its bar 1 underneath the phrase's bar 4, which is the groove turning
* around in the wrong place, and not something anyone should have to reason about while
* performing. A chop has no such constraint: it is sub-bar, so it fires on any of the
* 16 steps, with odd steps optionally pushed late by `swing`.
*/
export function hitsInBar(v: Voice, barIndex: number, bpm: number, swing = 0,
bufDurS = Infinity): Hit[] {
if (v.muted || v.gain <= 0) return []
const bar = barSeconds(bpm)
const slot = ((barIndex % PHRASE_BARS) + PHRASE_BARS) % PHRASE_BARS
if (v.bars > 0) {
if (slot % v.bars !== 0) return []
if (!v.cells[slot]) return []
return [{ offsetS: 0, rate: loopAtRate(v.durS, v.bars, bpm), holdS: v.bars * bar }]
}
const step = bar / CHOP_STEPS
const hits: Hit[] = []
for (let i = 0; i < CHOP_STEPS; i++) {
if (!v.cells[i]) continue
hits.push({
offsetS: i * step + (i % 2 === 1 ? swing * step : 0),
rate: 1,
holdS: Math.min(bufDurS, step * 4),
})
}
return hits
}
// ── the rack ─────────────────────────────────────────────────────────────────
type Scheduled = { src: AudioBufferSourceNode; at: number }
/**
* A look-ahead scheduler. `tick` runs on a timer, but nothing it decides depends on
* WHEN it ran: every event is placed at an absolute AudioContext time derived from
* the transport origin, so a late timer produces the same sound as an early one (or
* drops the event, if it is already too late — better a hole than a flam).
*/
export class Deck {
bpm = 123
swing = 0 // 0..0.5 — delay of odd 16ths, as a fraction of a 16th
private voices = new Map<string, { v: Voice; buf: AudioBuffer }>()
private timer?: number
private t0 = 0 // ctx time of bar 0 of the phrase
private nextBar = 0 // next bar index not yet scheduled
private live: Scheduled[] = []
private master: GainNode
onBar?: (barInPhrase: number) => void
static LOOKAHEAD = 0.35 // s of audio scheduled in advance
static TICK_MS = 60
constructor() {
const ctx = audioCtx()
this.master = ctx.createGain()
this.master.gain.value = 0.8
this.master.connect(ctx.destination)
}
get playing() { return this.timer !== undefined }
get masterGain() { return this.master.gain.value }
setMaster(v: number) { this.master.gain.value = v }
setVoice(v: Voice, buf: AudioBuffer) { this.voices.set(v.id, { v, buf }) }
removeVoice(id: string) { this.voices.delete(id) }
clear() { this.voices.clear() }
/** Where the playhead is, as a bar index within the phrase, or -1 when stopped. */
position(): number {
if (!this.playing) return -1
const el = audioCtx().currentTime - this.t0
return Math.floor(el / barSeconds(this.bpm)) % PHRASE_BARS
}
start() {
if (this.playing) return
const ctx = audioCtx()
this.t0 = ctx.currentTime + 0.12 // a beat of slack so bar 0 is not already late
this.nextBar = 0
this.timer = window.setInterval(() => this.tick(), Deck.TICK_MS)
this.tick()
}
stop() {
if (this.timer !== undefined) window.clearInterval(this.timer)
this.timer = undefined
for (const s of this.live) { try { s.src.stop() } catch { /* ended */ } }
this.live = []
}
private tick() {
const ctx = audioCtx()
const bar = barSeconds(this.bpm)
const until = ctx.currentTime + Deck.LOOKAHEAD
while (this.t0 + this.nextBar * bar < until) {
this.scheduleBar(this.nextBar, this.t0 + this.nextBar * bar)
this.nextBar++
}
this.live = this.live.filter((s) => s.at > ctx.currentTime - 30)
this.onBar?.(((this.nextBar - 1) % PHRASE_BARS + PHRASE_BARS) % PHRASE_BARS)
}
private scheduleBar(barIndex: number, at: number) {
for (const { v, buf } of this.voices.values()) {
for (const h of hitsInBar(v, barIndex, this.bpm, this.swing, buf.duration)) {
this.fire(buf, at + h.offsetS, v, h.rate, h.holdS)
}
}
}
private fire(buf: AudioBuffer, at: number, v: Voice, rate: number, holdS: number) {
const ctx = audioCtx()
if (at < ctx.currentTime) return // too late: a hole beats a flam
const src = ctx.createBufferSource()
src.buffer = buf
src.playbackRate.value = rate
const g = ctx.createGain()
g.gain.value = v.gain
// A 3 ms release on the tail. Cutting a voice at a hard sample boundary is a
// click, and a rack of eight clicking voices sounds like the kit is broken when
// it is the player that is.
const end = at + Math.min(holdS, buf.duration / rate)
g.gain.setValueAtTime(v.gain, Math.max(at, end - 0.003))
g.gain.linearRampToValueAtTime(0, end)
src.connect(g).connect(this.master)
src.start(at)
src.stop(end + 0.005)
this.live.push({ src, at })
}
}
/**
* The rack, written out as a `.tidal` block you can paste into a live file.
*
* This is the only reason the sequencer earns its place: clicking cells is a fast way
* to find out whether four loops belong together, but the answer is worthless unless
* it survives the trip to the rig. So the export is not a pretty-printer — it emits
* ParVagues' own idioms, on the orbits the kit's family maps to, with the mask
* pattern the cells describe, so the block plays the same thing the browser did.
*
* `loopAt` is the hinge: the deck plays every bar-loop at `dur / (bars * barlen)`,
* which is what `loopAt bars` does, so the same two numbers appear in both places.
*/
import { PHRASE_BARS, CHOP_STEPS, type Voice } from './engine'
/** Which `dN` a family belongs on, following the channel convention in `live/`. */
export const FAMILY_ORBIT: Record<string, number> = {
drums: 1, hits: 2, bass: 3, tonal: 5, vox: 6, fx: 8,
}
/** `mask "t f t t"` for a bar-loop row; `"t ~ f ~"`-style for a chop row.
* Returns null when every cell is on — a mask of all `t` is noise in the source. */
export function maskOf(cells: boolean[], stride: number, len: number): string | null {
const used: boolean[] = []
for (let i = 0; i < len; i += stride) used.push(!!cells[i])
if (used.every(Boolean)) return null
return used.map((b) => (b ? 't' : 'f')).join(' ')
}
/** A chop row's 16 steps as a Tidal structure string: `"x ~ ~ x ~ ~ x ~"`. */
export function stepsOf(cells: boolean[]): string {
return Array.from({ length: CHOP_STEPS }, (_, i) => (cells[i] ? 'x' : '~')).join(' ')
}
export type ExportRow = { voice: Voice; kit: string; n: number; family?: string | null }
export function toTidal(rows: ExportRow[], bpm: number, opts?: { swing?: number }): string {
const cps = bpm / 60 / 4
const out: string[] = [
`-- from the kit auditioner · ${bpm} bpm`,
`setcps (${cps.toFixed(4)}) -- ${bpm} bpm`,
'',
]
const usedOrbits = new Set<number>()
const nextOrbit = (family?: string | null) => {
let d = FAMILY_ORBIT[family ?? ''] ?? 4
while (usedOrbits.has(d)) d = (d % 12) + 1
usedOrbits.add(d)
return d
}
for (const r of rows) {
if (r.voice.muted) continue
const d = nextOrbit(r.family)
// The mixer fader is linear; Tidal's `gain` is quartic (amp = gain^4), so a fader
// at 0.5 is `gain (0.5 ** 0.25)` = 0.84, not `gain 0.5`. Getting this backwards is
// a 12 dB error and the reason the rack's fader is not just copied across.
const gain = r.voice.gain === 1 ? '' : ` # gain ${Math.pow(r.voice.gain, 0.25).toFixed(2)}`
if (r.voice.bars > 0) {
const m = maskOf(r.voice.cells, r.voice.bars, PHRASE_BARS)
const mask = m ? `mask "${m}" $ ` : ''
out.push(`d${d} $ ${mask}loopAt ${r.voice.bars} $ s "${r.kit}" # n ${r.n}${gain}`
+ ` -- ${r.voice.id.split('/')[1]}`)
} else {
const st = stepsOf(r.voice.cells)
out.push(`d${d} $ s "${st}" # s "${r.kit}" # n ${r.n}${gain}`
+ ` -- chop, ${r.voice.durS.toFixed(2)}s`)
}
}
if (opts?.swing) out.push('', `-- swing ${(opts.swing * 100).toFixed(0)}% — nudge by ear, `
+ `Tidal has no direct equivalent (try \`(# nudge 0.01)\` on the odd steps)`)
out.push('', 'hush')
return out.join('\n')
}
/** The same rack as a Strudel one-liner, for the browser playground (strudel.cc) or
* a `@strudel/web` pane. Emitted as text: the deck below already plays this rack, so
* a second audio graph would only add a way for the two to disagree. */
export function toStrudel(rows: ExportRow[], bpm: number): string {
const live = rows.filter((r) => !r.voice.muted)
if (!live.length) return 'silence'
const parts = live.map((r) => {
const g = r.voice.gain === 1 ? '' : `.gain(${r.voice.gain.toFixed(2)})`
if (r.voice.bars > 0) {
const m = maskOf(r.voice.cells, r.voice.bars, PHRASE_BARS)
const mask = m ? `.mask("<${m.replace(/ /g, ' ')}>")` : ''
return `s("${r.kit}").n(${r.n}).loopAt(${r.voice.bars})${mask}${g}`
}
return `s("${stepsOf(r.voice.cells).replace(/x/g, r.kit)}").n(${r.n})${g}`
})
return `setcps(${(bpm / 60 / 4).toFixed(4)})\nstack(\n ${parts.join(',\n ')}\n)`
}
/** The shape `tools/foundry/kitindex.py` writes to `public/kits.json`. */
export type Tier = 'S' | 'A' | 'B' | 'C' | 'D'
export type Family = 'drums' | 'hits' | 'bass' | 'tonal' | 'vox' | 'fx'
export type Sample = {
n: number // the index SuperDirt will answer to: `# n <n>`
name: string
file: string
url: string
sr: number
ch: number
peak_dbfs: number
rms_dbfs: number
env: number[]
// present only for kits the Foundry cut — a hand-made kit has none of this
bars: number | null
bpm: number | null
start_s: number | null // where in the source track it was cut from
dur_s: number | null
tier: Tier | null
grade: number | null
family: Family | null
stem_role: string | null
track: string | null
mode: 'loop' | 'chop' | null
flags: string[]
tags: Record<string, [string, number][]>
}
export type Kit = {
kit: string
n_samples: number
source: 'foundry' | 'legacy'
track: string | null
family: Family | null
bpm: number | null
samples: Sample[]
}
export type KitIndex = {
generated: string
root: string
peaks: number
kits: Kit[]
}
/** Family → the role colour it shares with the orbit rail elsewhere on the bridge. */
export const FAMILY_COLOR: Record<string, string> = {
drums: 'var(--color-percs)',
hits: 'var(--color-blocked)',
bass: 'var(--color-bass)',
tonal: 'var(--color-melodic)',
vox: 'var(--color-vox)',
fx: 'var(--color-atmos)',
}
export const familyColor = (f?: string | null) =>
FAMILY_COLOR[f ?? ''] ?? 'var(--color-ink-faint)'
/** Tempo, to two decimals, trailing zeros dropped: 123.05, 132.5, 120.
* One formatter so the transport, the rack header and every row agree — the same tempo
* printed two ways reads as a disagreement in the data. */
export const fmtBpm = (b: number) => String(Math.round(b * 100) / 100)
export const TIER_COLOR: Record<string, string> = {
S: 'var(--color-ready)', A: 'var(--color-tops)', B: 'var(--color-wip)',
C: 'var(--color-ink-faint)', D: 'var(--color-blocked)',
}
......@@ -26,6 +26,12 @@ function loadMounts(): [string, string][] {
* seek the big FLACs without copying them into the bundle. In production,
* `serve.py --dir dist` reads the same mounts, keeping /audio stable everywhere.
*/
const AUDIO_MIME: Record<string, string> = {
'.flac': 'audio/flac', '.wav': 'audio/wav', '.aif': 'audio/aiff',
'.aiff': 'audio/aiff', '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg',
'.opus': 'audio/ogg', '.m4a': 'audio/mp4',
}
function audioDevServer(): Plugin {
const mounts = loadMounts()
return {
......@@ -43,7 +49,11 @@ function audioDevServer(): Plugin {
const size = fs.statSync(file).size
const range = req.headers.range
res.setHeader('Accept-Ranges', 'bytes')
res.setHeader('Content-Type', 'audio/flac')
// by extension, not a constant: the masters are FLAC but the sample kits are
// WAV, and a wrong Content-Type is the kind of thing that works in one browser
// and silently refuses to decode in the next. serve.py uses guess_type for the
// same reason.
res.setHeader('Content-Type', AUDIO_MIME[path.extname(file).toLowerCase()] || 'application/octet-stream')
if (range) {
const m = /bytes=(\d*)-(\d*)/.exec(range)
const start = m && m[1] ? parseInt(m[1], 10) : 0
......@@ -76,6 +86,8 @@ export default defineConfig({
judge: path.resolve(__dirname, 'judge.html'),
// and the boundary lab — where exactly does one track become the next
bounds: path.resolve(__dirname, 'bounds.html'),
// La Cale — audition the sample kits the Foundry cuts, and rack them up
kits: path.resolve(__dirname, 'kits.html'),
},
},
},
......
__pycache__/
*.pyc
.pytest_cache/
# kitindex envelope cache — derived, keyed on file size+mtime
.kitindex-cache.json
......@@ -137,6 +137,102 @@ GET /media/<slug>/<path> → audio (HTTP Range / 206 — seek
---
## Producer stem packs (`engine.stempack`)
Not every source needs separating. A producer stem pack — Fred again..'s dropbox is the
worked example — arrives already split, by the human who mixed it, into stems named
`MAREA MIX10 123BPM KIT STEM.wav`. That is better material than demucs can produce and
completely outside what `find_takes` accepts, so `engine/stempack.py` is the batch path
for it (TODO #20).
```bash
cd tools/foundry
python3 -m engine.stempack "<pack dir>" --jobs 9 --no-link --out /tmp/cut # cut
python3 kitgate.py /tmp/cut/fred_kits.json # gate
python3 kitgate.py /tmp/cut/fred_kits.json --link # …then link
```
Cut with `--no-link` and let `kitgate.py` decide. It re-runs `grade` over the written
files (same verdict by a different code path — a disagreement means one of them is
wrong), runs the `kitcheck` audit, and checks for **bank shadowing before anything is
linked**: `loadSoundFiles` defaults to `appendToExisting = false`, so a folder whose
basename matches an existing bank frees that bank's buffers and takes the name, with no
error and no symptom except an old track playing the wrong sound.
`restage.py` applies the same kit-wide gain staging to kits already on disk — including
hand-cut ones, which carry whatever level their source had:
```bash
python3 restage.py --all-under ~/Work/Sound/Samples --dry-run
```
One kit per **track × role family** (`fred_marea_drums`, `fred_marea_vox`), files named
`NN_<producer role>_<bars>b.wav`, linked into Dirt-Samples so `s "fred_marea_drums"`
resolves. `fred_kits.tidal` is a paste-ready cheat sheet with the `n` index of every
sample, because a kit you cannot address is a kit you will not use.
### Four things this had to get right
**Resample before analysing.** Packs like this ship at 96 kHz, and librosa's hop/window
defaults are sample-rate-relative — at 96 k every analysis frame spans half the musical
time and beat tracking wanders. MAREA declares its tempo in the filename, so it is free
ground truth: at 96 kHz the finder returned 123/123/124/**119**/**128.1**/**128.5** bpm
with a 0.661 top score; at 44.1 kHz it returned 123.0–123.1 on every candidate with
0.859, in a third of the time. Rate is a correctness input, not a performance knob.
**One tempo per track.** `analyze_stem` reports each candidate's bpm as the mean local
BPM inside its own window — right for detecting drift, wrong for deriving a bar length,
because then every loop gets its own tempo and two loops from one track do not layer.
Bar length comes from the *mode* of the local-BPM histogram, with a confidence (the
share of beats within 5 % of it). Below 35 % there is no pulse worth cutting bars
against and the track ships chops only — ME, which has no drum stem at all, is the case
that motivated it.
**Modes rank in separate pools.** `grade` scores anything under 0.75 s as a one-shot on
`0.5·level + 0.3·dc + 0.2·zc` — no seam, no bar term. Export already removes DC and snaps
zero crossings, so chops land near 1.0 while loops pay 0.35 on seam and 0.25 on bar
consistency. Head to head, chops take every slot: measured on ME, chops 0.993–0.999,
loops 0.841–0.881, and the vocal kit came out as three stabs with no loops in it.
**Stage the level, because `amp = gain^4`.** Producer stems come off a loud master —
MAREA's ALL DRUMS and KIT both peak at +0.2 dBFS — so a loop cut from them arrives at full
scale, which `grade` calls clipping and SuperDirt cannot use: `# gain 1.2` is +7.6 dB and
the orbit clips the moment you touch it. Kits land with their hottest file at −6 dBFS, as
**one gain for the whole kit** (per-file normalisation would make a sparse hat loop as loud
as a full drum bus and throw away the balance two loops from one kit otherwise have for
free) and **never boosting** (a quiet stem is quiet on purpose).
**The label is evidence, not truth — and neither is the measurement.** `roles.py` maps
the pack's ~40 role tokens to a family and then measures the stem. Presence and bass are
decided by measurement (a stem under −40 dBFS peak has nothing in it; "all energy under
150 Hz, nothing above 2 kHz" is unambiguous). Everything else keeps the producer's label,
because onset density cannot separate a rhythmic rhodes from a drum bus — MAREA's KEYS2
reads 5.1 onsets/s and its sparse CLAP reads 1.0, so measurement alone would file both
in the wrong kit, silently. Disagreements are reported rather than resolved.
### What `kitcheck` catches that `grade` cannot
`grade` answers *is this mechanically a clean loop*. All of these pass it:
- **dead bars** — a 4-bar loop whose last two bars are a fade has a silent-to-silent
seam and an exact length, so it grades S. Per-bar RMS finds it.
- **not actually a loop** — the finder cuts on a beat grid, so its windows are always
bar-*aligned*; nothing checked they were a bar-length *unit*. Onset-envelope
autocorrelation at the bar lag does.
- **near-duplicates** — ALL DRUMS, KIT, MAIN DRUM LOOP and DRUM BREAKS are four views of
one groove, and the finder picks the same bar out of all four. Eight slots holding
three sounds is a worse kit than three. Dedup runs at export; `kitcheck` re-checks with
the same fingerprint and threshold so the two cannot disagree.
Two things about the periodicity test are worth knowing before reading its numbers.
Autocorrelation at lag *k* has only *N−k* overlapping frames, so normalising by `ac[0]`
scores identical material lower the shorter the loop — measured 0.418 / 0.623 / 0.754 at
2 / 4 / 8 bars on a perfect click track, which is length, not rhythm. And the floor is
**per family**, because the medians split hard by role (drums 0.754, fx 0.549, tonal 0.214,
bass 0.173, vox 0.087): a drum loop that does not repeat at the bar is broken, while a sung
phrase that does not repeat within itself is a sung phrase. One threshold for both condemned
12 of 20 vocal loops for being vocal.
## Engine roadmap — engine1 → engine2
The separation backend is a **registry** (`engine/separate.py`); the CLI/GUI ask
......
# The Foundry — TODO
Resume state for the URL→audio→stems→loops sampling tool. Snapshot **2026-06-29**.
Resume state for the URL→audio→stems→loops sampling tool. Snapshot **2026-08-21**.
Task IDs map to the session task board. Design: memory `project_foundry`.
## 🔵 In progress
......@@ -43,8 +43,90 @@ Task IDs map to the session task board. Design: memory `project_foundry`.
other top-3 BEFORE B/B/B → AFTER **S/S/S** (bad favorite @56.74 demoted out)
+2 rerank regression tests + 1 sparse-percussion test; suite 57→60 green.
## 🟢 Landed (2026-08-21) — #20 batch, via producer stem packs
`engine/stempack.py` + `engine/roles.py` + `engine/kitcheck.py` + `kitgate.py` +
`restage.py`, branch `claude/fred-kits`, 14 commits, suite 86 → 112.
Full write-up: `armada/tasks/027-the-label-and-the-lens.md`, memory
`reference_stempack` + `feedback_label_and_lens`.
```bash
cd tools/foundry
python3 -m engine.stempack "<pack>" --jobs 9 --no-link --out /tmp/cut
python3 kitgate.py /tmp/cut/fred_kits.json # then --link
```
## 🟡 In progress — finish the Fred pack (RESUME HERE)
**Staging root is `~/Work/Sound/Samples/Fred/output/<kit>/`** (PLN asked for it there,
2026-08-21), passed as `--samples-root`; Dirt-Samples symlinks point into it.
Runs 1-5 were all discarded, each superseded by a fix found by looking at its output:
161 samples (pre-dead-bar-fix), 153 (pre-rotation-fix), 153 (**24 of them digital
silence graded A/B**), aborted (wrong staging root), 129 (silence filtered at export
only, so BIGHEN's tonal kit lost 3 loops instead of gaining 3 real ones). **Run 6** is
the first with the finder itself refusing silent windows.
```bash
cd tools/foundry
python3 -u -m engine.stempack "$HOME/Work/Sound/Samples/Fred/fred again stems" \
--no-link --samples-root "$HOME/Work/Sound/Samples/Fred/output" --out <fresh dir>
```
`--jobs` now defaults to half the cores (children run at nice 19 / SCHED_IDLE — PLN
works on this machine). Takes ~20-30 min, fully deterministic.
**Exact steps to finish, in order:**
1. `python3 restage.py --cuts <out>/fred_kits.json --dry-run` — expect **0 kits**
changed (export stages, and restage has a 0.05 dB dead band so it is idempotent).
2. `python3 kitgate.py <out>/fred_kits.json` — five checks, must be green on grade /
kitcheck / shadowing / `n` indices. `rights` is advisory and WILL warn until step 3.
3. `python3 ../../armada/tide-table/register_fred_rights.py <out>/fred_kits.json`
— every `fred_*` bank as `third_party` **with provenance**, so per-track release
blocks by design. Before linking.
4. `python3 kitgate.py <out>/fred_kits.json --link` (takes each kit's root from the
manifest), then `python3 ../bank-check.py --since 1` to confirm 0 shadowed.
5. `python3 kitindex.py --root "$HOME/Work/Sound/Samples/Fred/output" --root \
"$HOME/Work/Sound/Samples" --cuts <out>/fred_kits.json` — the index La Cale reads.
6. Copy `fred_kits.{json,md,tidal}` + `kitcheck.md` into `packs/fred/` and commit
(`packs/README.md` — manifest, not audio).
7. Report: per-kit tier histogram, distinct-sound count, the "worth a second listen"
table, and the paste-ready `n` indices.
**Ground truth:** MAREA declares `123BPM` in its filenames; the grid must read
**123.0**. PLEASEMAKEITBETTER reads 20% tempo confidence and correctly ships
chops only.
## 🟢 Landed (2026-08-21) — #21 La Cale, the kit auditioner
`armada/ui/kits.html` + `src/kits/` + `tools/foundry/kitindex.py`. Browse every kit
under the sample root (the 111 that predate the Foundry included), audition a sample
untouched or looped, rack several on one bar grid at a chosen tempo, export a
paste-ready `.tidal` block. The rack plays `rate = dur / (bars * barlen)` — literally
`loopAt` — so what you hear is what the rig does. 13 vitest cases on the export.
```bash
cd armada/ui && nvm use 22 && npm ci && npm run dev # /kits.html
python3 ../serve.py --dir dist --port 8792 # LAN / phone
cd ../../tools/foundry && python3 kitindex.py --all # regenerate the index
```
Open follow-ups, none blocking: the Strudel tab emits text only (no `@strudel/web` —
the deck already plays the rack and two audio graphs could disagree); the rack has no
swing control wired to the UI though the engine supports it; and `?kits=fred_*` narrows
the rail but there is no way to save a rack as a named preset.
## 🔴 Open
- [ ] **#20 — Batch-explore many sources** (Foundry over the full corpus + merge).
- [ ] **#22 — Point stempack at the rest.** Same driver, no new code:
the 13 ACAPELLAS + 13 INSTRUMENTALS in `~/Work/Sound/Samples/Fred/` (already
44.1 kHz, so no resample; acapellas want chops mode), and the ~27 demucs catches in
`~/Downloads/separated/htdemucs/`. Each needs a `--prefix`.
- [ ] **#23 — Run `kitcheck`/`restage` over the whole existing corpus.** The dead-bar and
periodicity tests are general, and `Samples/` has 163 kits that have never been
audited; hand-cut kits also carry whatever level their source had.
`python3 restage.py --all-under ~/Work/Sound/Samples --dry-run` is the first look.
Expect "grades S, useless" to be non-rare.
- [ ] **#20b — Batch-explore the merged corpus** (the original #20 framing).
Workflow is one-catch-at-a-time; build batch exploration:
- Batch driver: `find_takes` over MANY source stems (the ~30 in
`~/Downloads/separated/htdemucs` + new fetches) → grade → surface the best
......
......@@ -40,6 +40,14 @@ THRESH = {
"rms_lo_dbfs": -28.0, # A-profile moderate-level band …
"rms_hi_dbfs": -12.0, # … RMS in here is ideal
"clip_dbfs": -0.1, # peak at/above this ⇒ clipping
# Peak below this ⇒ there is no audio here. NOT a taste threshold: every other
# sub-score is undefined on silence (the seam between two silences is perfect, the
# DC of silence is zero, the zero crossings are trivially fine), so a silent window
# scores 0.90 × perfect + 0.10 × nothing and comes out tier A. Measured on Fred
# again..'s pack: 24 of 153 cut loops were digital silence at -104…-75 dBFS peak,
# graded 0.707-0.896, and the next file up was -47.1 — a 28 dB empty gap, so the
# floor is placed inside it rather than tuned.
"empty_dbfs": -60.0,
"bars": (1, 2, 4, 8), # B-rule bar interpretations to test
"beats_per_bar": 4,
"tempo_lo": 60.0,
......@@ -256,6 +264,19 @@ def grade_array(y: np.ndarray, sr: int, *, path: str = "<array>",
grade = sum(w[k] * sub[k] for k in w)
flags: list[str] = []
# Presence is a PRECONDITION, not a weighted term. Everything above is a measure of
# how cleanly this audio loops, and all of it is vacuously true of silence — so the
# composite has a degenerate optimum at "nothing", which the finder duly walked into.
# A loop nobody can hear is not an A-grade loop whatever its seam looks like.
# Measured on the CHANNELS, not the mono sum: a deliberately wide stereo file can
# have almost no mono content — an anti-phase pair sums to exact zero — and calling
# that "empty" would both mis-report it and suppress the mono-incompatible flag that
# is the actually useful thing to say about it.
file_peak_db = float(20 * np.log10(float(np.max(np.abs(y))) + _EPS))
empty = file_peak_db < THRESH["empty_dbfs"]
if empty:
flags.append("empty")
grade = 0.0
if one_shot:
flags.append("one-shot")
if abs(dc) > THRESH["dc_ok"]:
......@@ -266,9 +287,15 @@ def grade_array(y: np.ndarray, sr: int, *, path: str = "<array>",
flags.append("off-grid")
if clipped:
flags.append("clipping")
if rms_db < -45:
if rms_db < -45 and not empty:
# RMS this low with a real peak means SPARSE, not silent — a hat loop with peaks
# at -14.7 reads -48.8 RMS because RMS measures how much silence it contains
# (`feedback_right_lens_per_control`). Advisory; `empty` is the blocking one.
flags.append("near-silent")
if (role == "bass" or y.ndim > 1) and corr < 0.2:
if (role == "bass" or y.ndim > 1) and corr < 0.2 and not empty:
# Interchannel correlation of two independent noise floors is ~0, so silence
# reads as maximally mono-incompatible; 4 of MAREA's flagged vox loops were
# just empty.
flags.append("mono-incompatible")
metrics = {
......
"""kitcheck — the post-export audit: what `grade` cannot see.
`grade.py` answers *is this file mechanically a clean loop* — seam, zero crossings,
DC, level, bar self-consistency. Every one of those can be perfect on a sample that
is musically useless, and this module exists for exactly the failures that survive a
clean grade. It is the substitute for an ear when nobody is listening yet.
Three checks, each closing a hole the rubric has by construction:
**1 · dead bars.** A four-bar loop whose last two bars are the tail of a fade grades
S: the seam is silent-to-silent, the length is exact. It is still a two-bar loop
with two bars of nothing after it. Per-bar RMS, flagged when any bar sits far
below the loudest.
**2 · bar periodicity.** The finder cuts on a beat grid, so its windows are always
*bar-aligned*; nothing checks they are a *bar-length musical unit*. Onset-envelope
autocorrelation at the half/whole-bar lag says whether the material actually
repeats at the claimed rate, or whether four bars of through-composed movement got
labelled a loop.
**3 · near-duplicates.** A producer pack ships overlapping stems on purpose — ALL
DRUMS, KIT, MAIN DRUM LOOP and DRUM BREAKS are four views of one groove. Cut the
best window from each and you ship the same bar four times under four names. A kit
whose 8 slots hold 3 distinct sounds is worse than a kit of 3.
Plus the two cheap invariants worth asserting rather than assuming: every bar-loop's
duration is an exact bar multiple at its own BPM, and every file is 44.1 kHz.
python3 -m engine.kitcheck fred_kits.json
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import numpy as np
import soundfile as sf
BAR_TOL_MS = 1.0 # a bar multiple is exact or it is not
DEAD_BAR_DB = 18.0 # a bar this far under the loudest is not carrying material
DUPE_CORR = 0.93 # normalised cross-correlation above this ⇒ same sound
# Bar periodicity is only diagnostic for material that is SUPPOSED to repeat inside
# itself. Measured over 81 bar-loops from the Fred pack, the medians split hard by role:
# drums 0.754, fx 0.549, tonal 0.214, bass 0.173, vox 0.087 — an 8.7x spread between the
# ends. A drum loop that does not repeat at the bar is broken; a sung phrase that does not
# repeat within itself is a sung phrase. Reading one number against one threshold would
# have condemned 12 of 20 vocal loops for the crime of being vocal
# (`feedback_right_lens_per_control`), so the threshold is per family and `None` means
# "measured and reported, never flagged".
# Peak below this ⇒ nothing in the file. Same number as `grade.THRESH["empty_dbfs"]`,
# imported rather than restated so the two lenses cannot drift apart.
from .grade import THRESH as _GRADE_THRESH # noqa: E402
EMPTY_DBFS = _GRADE_THRESH["empty_dbfs"]
PERIODICITY_FLOOR = {
"drums": 0.30, # p25 is 0.615 — 0.30 catches only genuine outliers
"hits": 0.30,
"fx": 0.05, # bimodal: rhythmic textures and drones both live here
"tonal": 0.05, # a held chord is a legitimate loop with no onset period
"bass": 0.05,
"vox": None, # never flagged
}
@dataclass
class KitCheck:
path: str
name: str
kit: str
ok: bool = True
problems: list = field(default_factory=list)
bar_dev_ms: Optional[float] = None
bar_rms_db: list = field(default_factory=list)
periodicity: Optional[float] = None
dupe_of: Optional[str] = None
def _mono(y):
return y.mean(axis=1) if y.ndim > 1 else y
def bar_levels(y: np.ndarray, sr: int, bars: int) -> list[float]:
"""RMS in dBFS of each bar of the loop."""
if bars <= 0:
return []
n = len(y) // bars
return [round(float(20 * np.log10(np.sqrt(np.mean(y[i * n:(i + 1) * n] ** 2)) + 1e-12)), 1)
for i in range(bars)]
def periodicity(y: np.ndarray, sr: int, bars: int) -> float:
"""How strongly the onset envelope repeats at the loop's own bar period (0..1).
Autocorrelation of the onset-strength envelope, read at the lag of one bar (and of
two bars, when the loop is long enough to have them). A rhythmic loop peaks there; a
through-composed window does not.
The normalisation matters more than the rest of the function. At lag k only N-k
frames overlap, so dividing the raw correlation by ac[0] — which sums all N — makes
every score a function of how much of the loop the lag consumes, and a 1-bar lag
consumes half of a 2-bar loop but an eighth of an 8-bar one. Measured on a perfectly
periodic click track, the biased estimator reports 0.418 / 0.623 / 0.754 at 2 / 4 / 8
bars for identical material; dividing by the overlap count first gives 0.834 / 0.830 /
0.862, flat as it must be. Without that correction the short loops look arrhythmic
purely for being short (`feedback_right_lens_per_control`).
"""
import librosa
if bars < 2:
return 1.0 # a 1-bar loop has no internal bar period
oenv = librosa.onset.onset_strength(y=y, sr=sr, hop_length=512)
oenv = oenv - oenv.mean()
if not np.any(oenv) or len(oenv) < 8:
return 0.0
ac = np.correlate(oenv, oenv, mode="full")[len(oenv) - 1:]
ac = ac / np.arange(len(oenv), 0, -1) # unbiased: per-lag overlap count
ac = ac / (ac[0] + 1e-12)
per_bar = len(oenv) / bars
best = 0.0
for k in (1, 2): # one bar, and two bars
lag = int(round(per_bar * k))
if 0 < lag < len(ac):
best = max(best, float(ac[max(0, lag - 2):lag + 3].max()))
return round(max(0.0, min(1.0, best)), 3)
def _fingerprint(y: np.ndarray, sr: int, n: int = 256) -> np.ndarray:
"""A short, tempo-agnostic signature: log-mel energy resampled to n columns.
Two cuts of the same groove at different lengths must still compare equal, so the
envelope is normalised in time (resampled to a fixed width) before comparison —
a raw waveform correlation would call a 2-bar and a 4-bar cut of one break
unrelated.
"""
import librosa
m = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=24, hop_length=512)
m = librosa.power_to_db(m + 1e-12)
if m.shape[1] < 2:
return np.zeros(24 * n)
idx = np.linspace(0, m.shape[1] - 1, n)
m = np.vstack([np.interp(idx, np.arange(m.shape[1]), row) for row in m])
m = m - m.mean()
return (m / (np.linalg.norm(m) + 1e-12)).ravel()
def _periodicity_floor(family: str) -> Optional[float]:
"""The floor this family's loops are held to, or None if the measure says nothing."""
return PERIODICITY_FLOOR.get(family, 0.05)
def check_cuts(cuts: list[dict]) -> list[KitCheck]:
"""Audit every exported sample, then cross-check for duplicates within each kit."""
out: list[KitCheck] = []
prints: dict[str, list[tuple[str, np.ndarray]]] = {}
for c in cuts:
r = KitCheck(path=c["path"], name=c["name"], kit=c["kit"])
p = Path(c["path"])
if not p.exists():
r.ok = False; r.problems.append("missing file"); out.append(r); continue
y, sr = sf.read(str(p), always_2d=True, dtype="float32")
m = _mono(y)
if sr != 44100:
r.ok = False; r.problems.append(f"sample rate {sr}, expected 44100")
# Presence, asked here as well as in `grade`, on purpose: the two answers come
# from different code and a disagreement means one of them is broken. This is
# the check that would have caught 24 silent loops shipping at tier A/B — the
# dead-bar test could not, because it compares bars to EACH OTHER and a
# uniformly silent clip has a perfectly flat profile.
peak_db = float(20 * np.log10(float(np.max(np.abs(y))) + 1e-12))
if peak_db < EMPTY_DBFS:
r.ok = False
r.problems.append(f"empty: peak {peak_db:.1f} dBFS — there is no audio here")
bars, bpm = c["bars"], c["bpm"]
if bars > 0 and bpm > 0:
want = bars * 4 * 60.0 / bpm
dev = (len(m) / sr - want) * 1000
r.bar_dev_ms = round(dev, 3)
if abs(dev) > BAR_TOL_MS:
r.ok = False
r.problems.append(f"not a bar multiple: {dev:+.1f} ms off {bars} bars @ {bpm:.1f}")
r.bar_rms_db = bar_levels(m, sr, bars)
if r.bar_rms_db:
lo, hi = min(r.bar_rms_db), max(r.bar_rms_db)
if hi - lo > DEAD_BAR_DB:
r.ok = False
dead = [i + 1 for i, v in enumerate(r.bar_rms_db) if hi - v > DEAD_BAR_DB]
r.problems.append(f"dead bar(s) {dead}: {hi - lo:.0f} dB spread across bars")
r.periodicity = periodicity(m, sr, bars)
floor = _periodicity_floor(c.get("family", ""))
if floor is not None and r.periodicity < floor:
r.problems.append(
f"weak bar periodicity ({r.periodicity:.2f} < {floor:.2f} for "
f"{c.get('family')}) — bar-aligned but not a repeating unit")
prints.setdefault(c["kit"], []).append((c["name"], _fingerprint(m, sr)))
out.append(r)
by_name = {r.name: r for r in out}
for kit, items in prints.items():
for i in range(len(items)):
for j in range(i + 1, len(items)):
corr = float(items[i][1] @ items[j][1])
if corr > DUPE_CORR:
later = by_name[items[j][0]]
if later.dupe_of is None:
later.dupe_of = items[i][0]
later.problems.append(
f"near-duplicate of {items[i][0]} in the same kit (r={corr:.2f})")
return out
def report(checks: list[KitCheck]) -> str:
bad = [c for c in checks if c.problems]
lines = [f"# kit check — {len(checks)} samples, "
f"{len(checks) - len(bad)} clean, {len(bad)} flagged", ""]
if not bad:
lines.append("No problems found.")
for kit in sorted({c.kit for c in bad}):
lines.append(f"## `{kit}`")
for c in [x for x in bad if x.kit == kit]:
mark = "✗" if not c.ok else "⚠"
lines.append(f"- {mark} `{c.name}` — " + "; ".join(c.problems))
lines.append("")
dupes = [c for c in checks if c.dupe_of]
lines += ["", f"**Distinct sounds:** {len(checks) - len(dupes)} of {len(checks)} "
f"({len(dupes)} near-duplicates)"]
return "\n".join(lines)
def main(argv=None) -> int:
import argparse
ap = argparse.ArgumentParser(prog="kitcheck", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("cuts_json", type=Path, help="fred_kits.json from engine.stempack")
ap.add_argument("--out", type=Path, default=None)
a = ap.parse_args(argv)
cuts = json.loads(a.cuts_json.read_text())
checks = check_cuts(cuts)
txt = report(checks)
print(txt)
if a.out:
a.out.write_text(txt)
(a.out.parent / (a.out.stem + ".json")).write_text(
json.dumps([c.__dict__ for c in checks], indent=2))
return 0 if all(c.ok for c in checks) else 1
if __name__ == "__main__":
raise SystemExit(main())
......@@ -196,6 +196,28 @@ def _true_seam(y: np.ndarray, s_smp: int, e_smp: int, sr: int, n_beats: int = 0)
return seam
# Amplitude below which a window has nothing in it. One number with `grade`, by import,
# so the finder and the grader cannot disagree about what silence is.
_EMPTY_AMP = 10.0 ** (G.THRESH["empty_dbfs"] / 20.0)
def _beat_peaks(y: np.ndarray, sr: int, times: np.ndarray) -> np.ndarray:
"""Peak amplitude in each inter-beat segment, so the presence gate is free.
A window's peak is the max over the beats it spans, and the segments partition
`[int(times[i]*sr), int(times[e]*sr))` exactly — the same samples the naive
`max(abs(slice))` would visit — so this is not an approximation. It just does the
O(samples) work once for the whole stem instead of once per candidate: at 8 bars a
window is ~700 k samples and there are thousands of windows, which turned a free
check into the most expensive line in the loop.
"""
mono = y if y.ndim == 1 else np.abs(y).max(axis=1)
a = np.abs(mono)
edges = [int(t * sr) for t in times]
return np.array([float(a[s:e].max()) if e > s else 0.0
for s, e in zip(edges, edges[1:])] + [0.0], dtype=np.float64)
def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8,
grid=None, weights=None, verify=True) -> list[LoopCandidate]:
"""Rank loop candidates within one stem.
......@@ -223,6 +245,7 @@ def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8,
ssm = beat_sync_ssm(y, sr, peaks) # peaks are onset-envelope frames
n = ssm.shape[0]
nov = np.maximum(foote_novelty(ssm, 2), foote_novelty(ssm, 8)) if n else np.zeros(nb)
beat_peak = _beat_peaks(y, sr, times) # for the presence gate, computed once
cands: list[LoopCandidate] = []
for B in bars:
......@@ -237,6 +260,15 @@ def analyze_stem(y: np.ndarray, sr: int, *, bars=(2, 4, 8), top_n=8,
sl = y[s_smp:e_smp]
if sl.size < 8:
continue
# Presence, BEFORE ranking. Every term below is vacuously perfect on silence
# — a seam between two silences is immaculate, its zero crossings are trivial,
# a flat SSM block is maximally self-similar — so the composite's optimum is
# "no audio" and a stem with long gaps spends all its candidate slots there.
# Rejecting at export (via `grade`) is not enough: BIGHEN's strings stem had
# all six of its top windows inside a rest, so the kit went from four silent
# loops to one real one instead of to four real ones.
if float(np.max(beat_peak[i:e])) < _EMPTY_AMP:
continue
seam, _ = G.seam_score(sl)
zc, _, _ = G.zc_score(sl, sr)
struct = _structural(ssm, i, e)
......@@ -473,8 +505,44 @@ ROTATE_ATTACK_WIN_S = 0.030 # attack window each side of a beat slot
ROTATE_LOWBAND_HZ = 200.0 # downbeat = strongest LOW-band attack (kick-weighted)
def _downbeat_slot_scores(clip: np.ndarray, sr: int, n_beats: int) -> np.ndarray:
"""Per-slot downbeat score: low-band circular attack + a small full-band term.
Factored out of `_rotate_to_downbeat` so the rotation can CHECK ITS OWN RESULT with
the identical scorer instead of a lookalike — measuring a rotation with a slightly
different window or weighting produces a disagreement that is the measurement's fault
(`feedback_check_the_instrument_first`).
"""
from scipy.signal import butter, sosfiltfilt
c2 = clip if clip.ndim == 2 else clip[:, None]
n = c2.shape[0]
mono = c2.mean(axis=1).astype(np.float64)
sos = butter(4, ROTATE_LOWBAND_HZ / (sr / 2), btype="low", output="sos")
low = sosfiltfilt(sos, mono)
w = max(8, int(ROTATE_ATTACK_WIN_S * sr))
def _rms(x: np.ndarray, a: int, b: int) -> float: # circular slice RMS
idx = np.arange(a, b) % n
return float(np.sqrt(np.mean(x[idx] ** 2) + 1e-12))
out = np.empty(n_beats)
for k in range(n_beats):
p = int(round(k * n / n_beats))
out[k] = ((_rms(low, p, p + w) - _rms(low, p - w, p))
+ 0.25 * (_rms(mono, p, p + w) - _rms(mono, p - w, p)))
return out
def _downbeat_miss(clip: np.ndarray, sr: int, n_beats: int) -> float:
"""How far slot 0 is from being the best slot: 0.0 = it IS the downbeat, 1.0 = worst."""
s = _downbeat_slot_scores(clip, sr, n_beats)
rng = float(s.max() - s.min())
return 0.0 if rng <= 0 else float((s.max() - s[0]) / rng)
def _rotate_to_downbeat(clip: np.ndarray, sr: int, n_beats: int,
force_slot: Optional[int] = None) -> tuple[np.ndarray, int, int]:
force_slot: Optional[int] = None,
passes: int = 3) -> tuple[np.ndarray, int, int]:
"""Rotate a bar-exact, clean-seam loop so the musical downbeat sits at position 0.
PLN's ear-finding (2026-07-11): loops came out 'timed ok but cut BCDA/DABC' — the PLP
......@@ -498,7 +566,22 @@ def _rotate_to_downbeat(clip: np.ndarray, sr: int, n_beats: int,
from the most rhythmic stem of the window and applied to all of them; independent
per-stem rotation could shift their relative groove by whole beats).
`passes` (default 3): rotate, then CHECK, and rotate again if slot 0 still is not the
strongest attack. One pass is not enough on real music, because the roll point is
zero-crossing snapped (up to ~10 ms, against a 20 ms attack window) and that is enough
to flip the winner between two near-tied slots. Measured over 60 shipped bar-loops from
the Fred pack: after one pass, slot 0 was the best slot in 29 and in the WORSE HALF of
slots in 10 — one loop in six starting on a weak beat, which is the "timed ok but cut
BCDA/DABC" complaint this function exists to fix. With up to three passes and a
strictly-improving guard: best slot in 46, worse half in 1, median miss 0.032 → 0.000.
Iterating is safe because rotation is free (see above): two rolls compose into one, and
the guard means a pass that does not improve the measured miss is discarded, so this
cannot oscillate.
`clip` is (n,) or (n, ch). Returns (rotated, best_slot, roll_samples); slot 0 ⇒ no-op.
`best_slot`/`roll_samples` describe the FIRST pass, so a caller re-deriving a shared
rotation with `force_slot` sees what it saw before.
"""
c2 = clip if clip.ndim == 2 else clip[:, None]
n = c2.shape[0]
......@@ -506,23 +589,7 @@ def _rotate_to_downbeat(clip: np.ndarray, sr: int, n_beats: int,
return clip, 0, 0
mono = c2.mean(axis=1).astype(np.float64)
if force_slot is None:
from scipy.signal import butter, sosfiltfilt
sos = butter(4, ROTATE_LOWBAND_HZ / (sr / 2), btype="low", output="sos")
low = sosfiltfilt(sos, mono)
w = max(8, int(ROTATE_ATTACK_WIN_S * sr))
def _rms(x: np.ndarray, a: int, b: int) -> float: # circular slice RMS
idx = np.arange(a, b) % n
return float(np.sqrt(np.mean(x[idx] ** 2) + 1e-12))
best_k, best_score = 0, -np.inf
for k in range(n_beats):
p = int(round(k * n / n_beats))
att_low = _rms(low, p, p + w) - _rms(low, p - w, p)
att_full = _rms(mono, p, p + w) - _rms(mono, p - w, p)
score = att_low + 0.25 * att_full
if score > best_score:
best_score, best_k = score, k
best_k = int(np.argmax(_downbeat_slot_scores(clip, sr, n_beats)))
else:
best_k = int(force_slot) % n_beats
if best_k == 0:
......@@ -531,6 +598,22 @@ def _rotate_to_downbeat(clip: np.ndarray, sr: int, n_beats: int,
rotated = np.roll(c2, -r, axis=0)
return (rotated if clip.ndim == 2 else rotated[:, 0]), best_k, r
# ── the check-and-repeat pass (see `passes` above) ────────────────────────
if force_slot is None and passes > 1:
cur, miss = out, _downbeat_miss(out, sr, n_beats)
for _ in range(passes - 1):
if miss <= 0.02: # slot 0 already is the downbeat
break
nxt, slot, _roll = _rotate_to_downbeat(cur, sr, n_beats, passes=1)
if slot == 0:
break
m = _downbeat_miss(nxt, sr, n_beats)
if m >= miss: # no improvement: keep what we have
break
cur, miss = nxt, m
out = cur
return out, best_k, r
def _pocket_zc(mono: np.ndarray, idx: int, sr: int, *,
back_s: float = 0.025, fwd_s: float = 0.002) -> int:
......
"""roles — map a producer-labelled stem filename to a role family, then VERIFY it.
Fred again's dropbox (and any producer stem pack) ships stems named by the human
who made them: `MAREA MIX10 123BPM KIT STEM.wav`, `ANGIE MIX18 PAD STUFF STEM.wav`.
That label is a strong prior and a terrible fact. The house rule stands — never
infer a sound's role from its name, validate by analysis (`feedback_mastering_eda`)
— so this module does both halves:
1. `parse_stem_name()` — filename → (track, subrole token) by stripping the
`<TRACK> MIX<n> [<bpm>BPM] <ROLE> STEM` scaffolding the pack uses.
2. `ROLE_MAP` — subrole token → the *claimed* family + a mode hint.
3. `probe()` — cheap EDA (level, silence, centroid, LF fraction,
onset density) that CONFIRMS or CONTRADICTS the claim, and is the only thing
downstream code is allowed to trust.
Families are the finder's four canonical stems plus `fx`, because a drone/noise/
texture stem behaves like none of the four and wants its own kit.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Optional
import numpy as np
# ── families ─────────────────────────────────────────────────────────────────
# `stem` is what the finder/export code keys on (bass mono-sum, rotation priority,
# grid preference); `family` is what names the kit.
FAMILIES = {
"drums": {"stem": "drums", "mode": "loops"},
"bass": {"stem": "bass", "mode": "loops"},
"vox": {"stem": "vocals", "mode": "both"},
"tonal": {"stem": "other", "mode": "loops"},
"fx": {"stem": "other", "mode": "both"},
"hits": {"stem": "drums", "mode": "chops"}, # isolated one-hit stems
}
# subrole token → (family, is_composite)
# `is_composite` marks a stem that already contains a full groove/arrangement —
# those are the loop gold (ALL DRUMS, KIT, BREAKS) vs an isolated element (KICK).
ROLE_MAP: dict[str, tuple[str, bool]] = {
# rhythmic composites — the loop gold
"all drums": ("drums", True), "drums": ("drums", True), "kit": ("drums", True),
"drum breaks": ("drums", True), "breaks": ("drums", True),
"main drum loop": ("drums", True), "drum loop 1": ("drums", True),
"lo drums": ("drums", True), "hi drums": ("drums", True),
"perc": ("drums", True), "hihats etc": ("drums", True), "hihats": ("drums", True),
# isolated hits — one-shot material, not loops
"kick": ("hits", False), "kicks": ("hits", False), "snare": ("hits", False),
"clap": ("hits", False), "toms": ("hits", False),
# low end
"bass": ("bass", True),
# voice
"lv": ("vox", True), "bv": ("vox", True), "f vox": ("vox", True),
"voices": ("vox", True), "choir": ("vox", True), "breaths": ("vox", False),
"vox textures": ("vox", True), "vox": ("vox", True),
# tonal
"synth": ("tonal", True), "keys": ("tonal", True), "keys1": ("tonal", True),
"keys2": ("tonal", True), "pad": ("tonal", True), "pad stuff": ("tonal", True),
"strings": ("tonal", True), "guitar": ("tonal", True), "organ": ("tonal", True),
"piano": ("tonal", True),
# texture / effect
"fx": ("fx", False), "drone": ("fx", False), "drones": ("fx", False),
"noise": ("fx", False), "textures": ("fx", True), "ipad textures": ("fx", True),
"morph": ("fx", False),
}
# scaffolding this pack wraps every role in
_SCAFFOLD = re.compile(
r"""^\s*
(?P<track>.*?) # TRACK (greedy-least)
\s+(?:MIX\d*|NEW)\s+ # MIX18 | MIX1 | NEW
(?:\d+\s*BPM\s+)? # optional 123BPM
(?P<role>.+?) # the role words
\s+STEM\s*$""",
re.IGNORECASE | re.VERBOSE,
)
_BPM_IN_NAME = re.compile(r"(\d{2,3})\s*BPM", re.IGNORECASE)
@dataclass
class StemName:
path_stem: str
track: Optional[str]
role_token: str # normalised, lowercase ("all drums")
claimed_family: str # from ROLE_MAP, or "tonal" when unknown
composite: bool
known: bool # was the token in ROLE_MAP?
bpm_hint: Optional[float] # producer-declared BPM, if the filename carried one
def parse_stem_name(filename: str) -> StemName:
"""`MAREA MIX10 123BPM KIT STEM.wav` → track=MAREA role=kit bpm_hint=123."""
base = re.sub(r"\.wav$", "", filename, flags=re.IGNORECASE)
bpm = _BPM_IN_NAME.search(base)
m = _SCAFFOLD.match(base)
if m:
track, role = m.group("track"), m.group("role")
else: # unscaffolded: last resort, whole name
track, role = None, re.sub(r"\s+STEM$", "", base, flags=re.IGNORECASE)
token = re.sub(r"\s+", " ", role.strip().lower())
fam, comp = ROLE_MAP.get(token, (None, None))
# a track-specific token (MAREA STEM, TONI VOX, MAKE IT THRU VOX) — fall back on
# the words it contains rather than giving up.
if fam is None:
for key, (f, c) in ROLE_MAP.items():
if re.search(rf"\b{re.escape(key)}\b", token):
fam, comp = f, c
break
return StemName(
path_stem=base, track=track, role_token=token,
claimed_family=fam or "tonal", composite=True if comp is None else comp,
known=fam is not None,
bpm_hint=float(bpm.group(1)) if bpm else None,
)
# ── the verification half ────────────────────────────────────────────────────
@dataclass
class StemProbe:
"""Cheap EDA that either backs the filename's claim or overrides it."""
rms_dbfs: float
peak_dbfs: float
active_frac: float # share of frames within 40 dB of this stem's own peak
active_rms_dbfs: float # how loud it is WHEN it plays
centroid_hz: float
lf_frac: float # energy below 150 Hz / total
hf_frac: float # energy above 2 kHz / total
onset_rate: float # onsets per second over the loud excerpts
env_crest: float # frame-RMS crest: ~1 sustained, >3 struck (gates onset_rate)
measured_family: str
agrees: bool = field(default=False)
note: str = ""
@property
def usable(self) -> bool:
"""Is there material here at all?
Judged on PEAK and on how loud the stem is when it plays — not on overall RMS,
which on a sparse stem measures how much silence it contains. ANGIE's HIHATS
stem is −48.8 dBFS RMS and 9.7 % active, and it is a perfectly good hi-hat
stem peaking at −14.7 (`feedback_right_lens_per_control`).
"""
return self.peak_dbfs > -40.0 and self.active_frac >= 0.005
def _loud_windows(db: np.ndarray, y: np.ndarray, sr: int, hop: int, *,
n: int = 4, win_s: float = 20.0) -> np.ndarray:
"""Excerpts chosen where the stem is PLAYING, concatenated.
Classifying a five-minute stem does not need five minutes of spectra, but the
cheap version — evenly spaced windows — samples the gaps. Measured on ANGIE:
even windows read HIHATS at −60.9 dBFS / 1.4 % active where the full file says
−48.8 / 9.7 %, and missed PAD STUFF's material completely (0.0 % vs 6.9 %),
because that stem only plays in two sections. A stem is not stationary, so where
you look decides what you find (`feedback_measure_the_time_axis`).
So: rank non-overlapping windows by energy and take the loudest few.
"""
win = int(win_s * sr)
if len(y) <= win * n:
return y
per = max(1, win // hop)
nwin = len(db) // per
if nwin <= n:
return y
energy = np.array([db[i * per:(i + 1) * per].mean() for i in range(nwin)])
picks = sorted(np.argsort(energy)[-n:])
return np.concatenate([y[i * per * hop:i * per * hop + win] for i in picks])
def probe(y: np.ndarray, sr: int, claimed: str) -> StemProbe:
"""Measure a mono stem and decide what it actually is. Name is never trusted.
Note that `env_crest` is a sparsity measure, not a percussion one — MAREA's single
vocal phrase reads 17.98 — which is why it is only ever used to *gate* onset density,
never on its own.
"""
import librosa
eps = 1e-12
hop = 512
peak = float(20 * np.log10(np.max(np.abs(y)) + eps))
rms = float(20 * np.log10(np.sqrt(np.mean(y ** 2)) + eps))
# Level and activity come from the WHOLE file — it is only a frame-RMS pass, and
# it is the number that decides whether a stem is worth cutting at all.
frame_db = 20 * np.log10(librosa.feature.rms(y=y, hop_length=hop)[0] + eps)
# "Active" relative to this stem's own peak, not an absolute floor: an isolated
# hi-hat stem is mostly silence by design, and its overall RMS is a statement about
# how much silence it contains, not about whether it has material in it.
active = (frame_db > peak - 40.0) & (frame_db > -60.0)
active_frac = float(active.mean()) if active.size else 0.0
active_rms = float(frame_db[active].mean()) if active.any() else -120.0
# Spectral shape and onsets, on the loud excerpts only.
w = _loud_windows(frame_db, y, sr, hop)
S = np.abs(librosa.stft(w, n_fft=2048, hop_length=hop)) ** 2
freqs = librosa.fft_frequencies(sr=sr, n_fft=2048)
tot = S.sum() + eps
band = S.sum(axis=1)
centroid = float((band * freqs).sum() / tot)
lf_frac = float(band[freqs < 150].sum() / tot)
hf_frac = float(band[freqs > 2000].sum() / tot)
onsets = librosa.onset.onset_detect(y=w, sr=sr, units="time", backtrack=True)
onset_rate = float(len(onsets) / max(len(w) / sr, 1.0))
# Onset COUNT is not a percussiveness measure. librosa's peak-picker finds peaks in
# whatever envelope it is handed, including a flat one: a steady 50 Hz sine yields 84
# onsets over six seconds (14/s), which would file a pure bass tone as drums. So the
# count is gated on the envelope actually having transients in it — the crest of the
# frame-RMS, ~1.0 for a sustained tone against 3.1-9.5 for anything struck.
fr = librosa.feature.rms(y=w, hop_length=hop)[0]
env_crest = float(np.percentile(fr, 99) / (fr.mean() + eps))
# ── the verdict ──────────────────────────────────────────────────────────
# PERCUSSIVE FIRST. The obvious test — "mostly low energy, low centroid ⇒ bass" —
# fires on a drum bus, because a modern kick carries most of a drum mix's ENERGY:
# measured on MAREA and ANGIE, ALL DRUMS reads lf_frac 0.84/0.86 and centroid
# 182/240 Hz, indistinguishable from a bass stem on those two features alone. What
# separates them is onset density (ALL DRUMS 6.6/s, BASS 1.2/s), so density is
# asked first and the spectral test only sees what is left.
if onset_rate > 3.0 and env_crest > 2.0:
measured = "drums"
elif hf_frac > 0.25 and onset_rate > 0.8:
measured = "drums" # bright and struck: a clap/snare stem is sparse
elif lf_frac > 0.75 and hf_frac < 0.05:
measured = "bass"
elif onset_rate < 0.35:
measured = "fx" # sustained/sparse: pad, drone, texture
elif onset_rate > 1.2 and centroid > 1200:
measured = "drums"
else:
measured = "tonal"
p = StemProbe(rms_dbfs=round(rms, 2), peak_dbfs=round(peak, 2),
active_frac=round(active_frac, 3), active_rms_dbfs=round(active_rms, 2),
centroid_hz=round(centroid, 1), lf_frac=round(lf_frac, 3),
hf_frac=round(hf_frac, 4), onset_rate=round(onset_rate, 2),
env_crest=round(env_crest, 2), measured_family=measured)
# `vox` and `tonal` are not separable by these cheap features (a sung line and a
# rhodes chord have the same coarse shape) — CLAP does that job downstream. So a
# vox claim is only CONTRADICTED when the measurement says bass or drums.
claim_stem = FAMILIES.get(claimed, FAMILIES["tonal"])["stem"]
meas_stem = FAMILIES.get(measured, FAMILIES["tonal"])["stem"]
soft = {"vox", "tonal", "fx"}
p.agrees = (claim_stem == meas_stem) or (claimed in soft and measured in soft)
if not p.agrees:
p.note = f"filename says {claimed}, measurement says {measured}"
if not p.usable:
p.note = (p.note + "; " if p.note else "") + \
f"no material (peak {peak:.0f} dBFS, active {active_frac:.1%})"
return p
def effective_family(name: StemName, p: StemProbe) -> str:
"""What we actually treat the stem as — and how much authority measurement has.
The house rule is that a sound's role is never *inferred* from its name; it is not
that a producer's label about their own stem is worthless. These two claims have
very different evidential weight, and the split matters:
* **Presence is measured, always.** Whether there is material in a stem is decided
by the probe and nothing else.
* **Bass is measured.** "Almost all energy under 150 Hz, essentially nothing above
2 kHz, not densely struck" is unambiguous, and a stem like that is bass whatever
the filename says.
* **Everything else keeps the label.** Onset density cannot separate a rhythmic
rhodes from a drum bus — measured on MAREA, KEYS2 reads 5.1 onsets/s and would be
filed as drums, and the sparse CLAP stem reads 1.0/s and would be filed as tonal.
Both are wrong, and both would be wrong *silently*, which is worse than the
label's known imprecision.
Every disagreement is reported either way, so a wrong label stays visible instead of
being quietly overwritten by a wrong measurement.
"""
if p.agrees:
return name.claimed_family
if not name.known:
# not the producer's word — our own fallback for a token like `MAREA STEM`.
# A guess loses to a measurement.
return p.measured_family
if (name.claimed_family in ("tonal", "fx") and p.measured_family == "bass"
and p.lf_frac > 0.75 and p.hf_frac < 0.05):
return "bass"
return name.claimed_family
"""stempack — batch the Foundry over a folder of ALREADY-separated producer stems.
The Foundry's engine1 exists to *make* stems (yt-dlp → demucs). A producer stem
pack skips that entirely: the stems arrive labelled by the human who mixed them
(`MAREA MIX10 123BPM KIT STEM.wav`). This module is the missing batch driver
(TODO #20) for that case — walk a pack, cut every stem, grade, and land one
SuperDirt kit per track × role family.
python3 -m engine.stempack "<pack dir>" --tracks MAREA ANGIE --prefix fred
Three things it does that `find_takes` could not:
1. **Resample to 44.1 kHz before analysis.** These packs ship at 96 kHz, and
librosa's hop/window defaults are sample-rate-relative — at 96 k every analysis
frame spans half the musical time and beat tracking wanders. Measured on
`MAREA … 123BPM KIT`, whose filename is free ground truth: at 96 k the finder
returned 123/123/124/**119**/**128.1**/**128.5** bpm with a 0.661 top score; at
44.1 k it returned 123.0–123.1 on every candidate with a 0.859 top score, in a
third of the time. Rate is a correctness input here, not a performance knob.
2. **Role names beyond the demucs four.** `find_takes` keys stems by
drums/bass/other/vocals — a dict that collides on KEYS1+KEYS2 and an export
`keep=` filter that would silently drop every stem in this pack. `roles.py`
maps the pack's ~40 tokens to a family and then *verifies* the label by
measurement before anything downstream trusts it.
3. **Loop-vs-chop by family, arbitrated by grade.** Composite rhythmic stems want
bar loops; isolated hits and voices want chops; where the family is ambiguous
both are run and `grade` picks. Every bar loop is exported at an exact bar
multiple so `loopAt <bars>` is correct by construction.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Callable, Optional
import numpy as np
import soundfile as sf
from . import grade as G
from . import loops as L
from . import roles as R
ANALYSIS_SR = 44100 # see module docstring §1 — a correctness constant
MIN_TIER = "B" # below this a loop does not ship
TIER_ORDER = ["D", "C", "B", "A", "S"]
@dataclass
class CutLoop:
"""One exported loop, with everything needed to play and to justify it."""
kit: str
name: str
path: str
track: str
stem_role: str # the producer's own token ("all drums")
family: str # our effective family, post-verification
mode: str # "loop" | "chop"
bars: int
bpm: float
dur_s: float
start_s: float
finder_score: float
tier: str
grade: float
flags: list
tags: dict # CLAP multi-axis tags, filled in later (may be {})
def _load_44k(path: Path) -> tuple[np.ndarray, np.ndarray, int]:
"""Read a stem → (stereo (n,ch) @44.1k, mono (n,) @44.1k, source_sr).
Everything downstream — the grid, the candidate times, the zero-crossing snap
and the written audio — lives at one rate, so the file that ships is exactly
the signal that was measured.
"""
import librosa
y, sr = sf.read(str(path), always_2d=True, dtype="float32")
if sr != ANALYSIS_SR:
# soxr_mq, not the soxr_hq default: ~3x faster and its passband is already
# far cleaner than anything the downstream onset/MFCC features can resolve.
y = librosa.resample(y.T, orig_sr=sr, target_sr=ANALYSIS_SR,
res_type="soxr_mq").T
return np.ascontiguousarray(y), y.mean(axis=1), sr
def _slice_export(y_st: np.ndarray, cand, *, rotate: bool = True,
mono_sum: bool = False, bar_bpm: float = 0.0) -> tuple[np.ndarray, int]:
"""Cut a candidate out of the stereo stem honouring the B-rules.
Reuses `loops`' own primitives rather than restating them: the length-preserving
zero-crossing snap (so the duration stays an exact bar multiple), the downbeat
rotation, and the DC removal. A chop (bars=0) gets per-edge snapping instead,
since there is no bar length to preserve.
Rotation is per-loop here, not per-shared-window as in `export_take`. That is the
right call for kits split by family: a loop rotated to its own downbeat starts on
its 1, so ANY two loops in the pack layer in phase — which is exactly what you
want when `fred_marea_drums` and `fred_marea_bass` are cut from different moments
of the track and still have to sit together under `loopAt`.
"""
sr = ANALYSIS_SR
mono_ref = y_st[:, 0]
a = int(cand.start_s * sr)
if cand.bars > 0:
# the TRACK tempo, not this window's local mean — see `track_tempo`
length = L.bar_len_samples(cand.bars, bar_bpm or cand.bpm, sr)
a, b = L._snap_length_preserving(mono_ref, a, sr, length)
else:
b = L._snap_zc(mono_ref, int(cand.end_s * sr), sr)
a = L._snap_zc(mono_ref, a, sr)
clip = y_st[a:b]
if cand.bars > 0 and rotate:
clip, _, _ = L._rotate_to_downbeat(clip, sr, cand.bars * L.BEATS_PER_BAR)
if mono_sum and clip.shape[1] > 1:
clip = clip.mean(axis=1, keepdims=True)
return clip - clip.mean(axis=0, keepdims=True), sr
def _best_candidates(y_mono: np.ndarray, family: str, grid, *, per_stem: int,
bars: tuple) -> dict[str, list]:
"""Run the mode(s) this family calls for, keeping the modes SEPARATE.
They must not compete on grade, because the rubric scores them on different weights
and the comparison is rigged. `grade` classes anything under 0.75 s as a one-shot and
scores it `0.5·level + 0.3·dc + 0.2·zc` — no seam term, no bar term, since a one-shot
is never looped. Export already removes DC and snaps to zero crossings, so almost
every chop lands near 1.0, while a loop pays 0.35 on seam and 0.25 on bar consistency
and rarely maxes both.
Measured on ME: every chop graded 0.993–0.999 (S), every loop 0.841–0.881 (A). Ranked
head to head, chops take every slot on a "both" family and the vocal kits come out as
stabs with no loops in them — precisely backwards. So each mode gets its own slots.
"""
mode = R.FAMILIES.get(family, R.FAMILIES["tonal"])["mode"]
out: dict[str, list] = {}
if mode in ("loops", "both"):
out["loop"] = L.analyze_stem(y_mono, ANALYSIS_SR, bars=bars, top_n=per_stem,
grid=grid)
if mode in ("chops", "both"):
out["chop"] = L.analyze_chops(y_mono, ANALYSIS_SR, top_n=per_stem)
return out
def _rank_by_grade(by_mode: dict[str, list], y_st: np.ndarray, family: str,
keep: int, *, bar_bpm: float = 0.0) -> list:
"""Materialise each candidate exactly as it would be written, then grade THAT.
This is `analyze_stem`'s `verify` discipline taken one step further: the finder
re-scores its seam on the post-snap window, and here the whole rubric is applied to
the post-snap, post-rotation, DC-removed, mono-summed clip — the actual bytes. A
candidate cannot score well on a signal that never ships.
Ranking happens WITHIN a mode (see `_best_candidates`); the modes are then
concatenated so both survive into the kit.
"""
mono_sum = family == "bass"
picked = []
for mode, cands in by_mode.items():
scored = []
for c in cands:
try:
clip, sr = _slice_export(y_st, c, mono_sum=mono_sum,
bar_bpm=bar_bpm)
except Exception:
continue
if clip.shape[0] < sr // 8: # < 125 ms: not a sample
continue
g = G.grade_array(clip.T, sr, path=f"<{mode}@{c.start_s}>",
role=R.FAMILIES.get(family, {}).get("stem"))
if c.bars > 0 and _has_dead_bars(clip, sr, c.bars):
continue # rejected here rather than reported later — see below
scored.append((mode, c, g, clip))
# grade first, finder score as the tiebreak — two clips can both be mechanically
# perfect, and then musical structure is the only thing left to separate them.
scored.sort(key=lambda x: (-x[2].grade, -x[1].score))
# a chop is cheap and a loop is the point: chops get half the slots, minimum one.
picked += scored[: keep if mode == "loop" else max(1, keep // 2)]
return picked
def track_tempo(grid) -> tuple[float, float]:
"""One tempo for the whole track, plus how much to trust it. (bpm, confidence).
`analyze_stem` reports each candidate's bpm as the MEAN of the local per-beat BPM
inside its own window, which is the right number for detecting drift but the wrong
one to derive a bar length from: every loop then gets its own idiosyncratic tempo
and two loops from the same track no longer layer. Measured on ME, the track grid
read 129.2 bpm while the two shipped loops were cut at 150.5.
So the bar length comes from a single robust track tempo — the mode of the local-BPM
histogram, not the mean, because a handful of dropped or doubled beats drag a mean
and leave a mode alone. Confidence is the share of beats sitting within ±5 % of it,
which is what tells us whether there is a pulse here at all.
"""
if grid is None or len(grid[2]) < 8:
return 0.0, 0.0
bpm = np.asarray(grid[2], dtype=float)
bpm = bpm[(bpm > 50) & (bpm < 220)]
if bpm.size < 8:
return 0.0, 0.0
hist, edges = np.histogram(bpm, bins=np.arange(50, 221, 1.0))
centre = edges[int(np.argmax(hist))] + 0.5
near = np.abs(bpm - centre) <= centre * 0.05
if near.sum() >= 4: # refine on the beats that agree
centre = float(np.median(bpm[near]))
near = np.abs(bpm - centre) <= centre * 0.05
return round(float(centre), 2), round(float(near.mean()), 3)
TEMPO_TRUST = 0.35 # below this share of on-tempo beats there is no usable grid
def _has_dead_bars(clip: np.ndarray, sr: int, bars: int) -> bool:
"""Does this loop stop partway through? Rejected at export, not merely flagged.
A four-bar loop whose last two bars are the tail of a fade is a two-bar loop with two
bars of silence stapled on, and `grade` cannot object to it: silent-to-silent is a
perfect seam and the length is still an exact bar multiple, so it grades S. Ten of the
first 161 cuts were exactly this. It is an objective defect with an objective test, so
it belongs in the gate rather than in a report somebody has to read.
"""
from .kitcheck import DEAD_BAR_DB, bar_levels
lv = bar_levels(clip.mean(axis=1) if clip.ndim > 1 else clip, sr, bars)
return bool(lv) and (max(lv) - min(lv)) > DEAD_BAR_DB
def _pick_grid(stems: list[dict], progress) -> tuple[Optional[tuple], Optional[str]]:
"""One grid for the whole track, from its most rhythmic *verified* stem.
`find_takes` picks by name (`drums` → `bass` → first). Here the pick is by
measurement: the highest onset rate among stems that measured as drums, so a
mislabelled or near-empty drum stem cannot set the grid every other stem is
cut against.
"""
rhythmic = [s for s in stems if s["probe"].measured_family == "drums"
and s["probe"].usable]
pool = rhythmic or [s for s in stems if s["probe"].usable]
if not pool:
return None, None
lead = max(pool, key=lambda s: s["probe"].onset_rate)
if progress:
progress(f" grid ← {lead['name'].role_token} "
f"(onset {lead['probe'].onset_rate:.1f}/s)")
return L.beat_grid(lead["mono"], ANALYSIS_SR), lead["name"].role_token
def analyze_track(track_dir: Path, *, per_stem: int = 6, keep_per_stem: int = 3,
bars: tuple = (1, 2, 4, 8),
progress: Optional[Callable[[str], None]] = None) -> dict:
"""Load every stem of one track, verify it, and rank its best cuttable material."""
wavs = sorted(p for p in track_dir.glob("*.wav"))
stems = []
for p in wavs:
name = R.parse_stem_name(p.name)
y_st, y_mono, src_sr = _load_44k(p)
pr = R.probe(y_mono, ANALYSIS_SR, name.claimed_family)
fam = R.effective_family(name, pr)
if progress:
mark = "·" if pr.agrees else "≠"
skip = "" if pr.usable else " SKIP"
progress(f" {mark} {name.role_token:18.18} {fam:6} "
f"rms {pr.rms_dbfs:6.1f} act {pr.active_frac:4.0%} "
f"cen {pr.centroid_hz:6.0f}Hz on {pr.onset_rate:4.1f}/s"
f"{(' ' + pr.note) if pr.note else ''}{skip}")
stems.append({"path": p, "name": name, "probe": pr, "family": fam,
"st": y_st, "mono": y_mono, "src_sr": src_sr})
usable = [s for s in stems if s["probe"].usable]
grid, grid_from = _pick_grid(usable, progress)
grid_bpm, tempo_conf = track_tempo(grid)
bar_ok = tempo_conf >= TEMPO_TRUST
if progress:
progress(f" tempo {grid_bpm:.1f} bpm, confidence {tempo_conf:.0%}"
+ ("" if bar_ok else " ⇒ NO usable pulse: chops only, no bar-loops"))
# the producer sometimes declares the tempo in the filename — free ground truth
declared = next((s["name"].bpm_hint for s in stems if s["name"].bpm_hint), None)
if declared and grid_bpm:
err = abs(grid_bpm - declared) / declared * 100
if progress:
verdict = "✓" if err < 2 else "⚠"
progress(f" {verdict} grid {grid_bpm:.1f} bpm vs declared {declared:.0f} "
f"({err:.1f}% off)")
for s in usable:
if progress:
progress(f" cutting {s['name'].role_token}…")
cands = _best_candidates(s["mono"], s["family"], grid,
per_stem=per_stem, bars=bars)
if not bar_ok:
cands.pop("loop", None) # a bar-loop off a tempo we do not believe
s["picks"] = _rank_by_grade(cands, s["st"], s["family"], keep_per_stem,
bar_bpm=grid_bpm if bar_ok else 0.0)
return {"dir": track_dir, "stems": stems, "usable": usable,
"grid_bpm": grid_bpm, "tempo_conf": tempo_conf, "bar_ok": bar_ok,
"grid_from": grid_from, "declared_bpm": declared}
# ── export ───────────────────────────────────────────────────────────────────
def _tidal_token(s: str) -> str:
"""A sample-name token Tidal and the filesystem both like."""
return re.sub(r"[^a-z0-9]+", "", s.lower())
def export_track(result: dict, *, prefix: str, samples_root: Optional[Path] = None,
min_tier: str = MIN_TIER, link: bool = True,
progress: Optional[Callable[[str], None]] = None) -> list[CutLoop]:
"""Write one kit per track × family and return what landed.
Kit = `<prefix>_<track>_<family>`; file = `NN_<producer role>_<bars>b.wav`. The
bar count is in the NAME because that is the number you need at the keyboard:
`loopAt 4 $ s "fred_marea_drums" # n 0` is only right if that file really is
four bars, and `kit_multiples_check` proves it after the fact.
"""
from . import publish
root = samples_root or publish.samples_root()
floor = TIER_ORDER.index(min_tier)
track_slug = _tidal_token(result["dir"].name)
by_kit: dict[str, list] = {}
for s in result["usable"]:
for mode, cand, g, clip in s.get("picks", []):
if TIER_ORDER.index(g.tier) < floor:
continue
by_kit.setdefault(f"{prefix}_{track_slug}_{s['family']}", []).append(
(s, mode, cand, g, clip))
out: list[CutLoop] = []
for kit, items in sorted(by_kit.items()):
items.sort(key=lambda t: (-t[3].grade,))
items = _drop_duplicates(items)
kit_dir = root / kit
kit_dir.mkdir(parents=True, exist_ok=True)
written: list[Path] = []
# one gain for the kit, computed before anything is written
head = kit_headroom_gain([float(20 * np.log10(np.max(np.abs(c[4])) + 1e-12))
for c in items])
for i, (s, mode, cand, g, clip) in enumerate(items):
role_tok = _tidal_token(s["name"].role_token) or "loop"
suffix = f"{cand.bars}b" if cand.bars > 0 else "chop"
dest = kit_dir / f"{i:02d}_{role_tok}_{suffix}.wav"
clip = clip * head
if head != 1.0:
# re-grade the STAGED clip: several sub-scores have a level term, so the
# grade computed before staging describes audio that is not what shipped.
# Selection above ran on the pre-staging grades, which is fine — the gain
# is uniform across the kit, so it cannot reorder its members — but the
# number recorded in the manifest has to be the one you get by grading
# the file on disk, or `kitgate`'s cross-check is comparing two different
# signals and calling the difference a defect.
g = G.grade_array(clip.T, ANALYSIS_SR, path=str(dest),
role=R.FAMILIES.get(s["family"], {}).get("stem"))
sf.write(str(dest), clip, ANALYSIS_SR, subtype="PCM_24")
written.append(dest)
out.append(CutLoop(
kit=kit, name=dest.stem, path=str(dest), track=result["dir"].name,
stem_role=s["name"].role_token, family=s["family"], mode=mode,
bars=cand.bars,
bpm=round(result["grid_bpm"] or cand.bpm, 2),
dur_s=round(clip.shape[0] / ANALYSIS_SR, 4),
start_s=cand.start_s, finder_score=cand.score,
tier=g.tier, grade=g.grade, flags=list(g.flags), tags={}))
# only bar-loops have a bar multiple to be off; a chop is sub-bar by definition,
# and checking it produces a warning that means nothing.
bar_files = [w for w, it in zip(written, items) if it[2].bars > 0]
chk = L.kit_multiples_check(bar_files, result["grid_bpm"] or 120.0)
if progress:
bad = "" if chk.get("ok", True) else " ⚠ off-grid rows"
hd = "" if head >= 1.0 else f" −{-20 * np.log10(head):.1f} dB headroom"
progress(f" → {kit:34.34} {len(written):2} files{bad}{hd}")
if link:
try:
publish.link_kit(kit, samples=root)
except Exception as e: # never fail a cut on a link
if progress:
progress(f" (link skipped: {e})")
return out
HEADROOM_CEILING_DB = -6.0 # kit-wide peak ceiling; see `kit_headroom_gain`
def kit_headroom_gain(peaks_dbfs: list[float], ceiling_db: float = HEADROOM_CEILING_DB
) -> float:
"""ONE attenuation for a whole kit, so its loudest file sits at `ceiling_db`.
These are stems from a loud commercial master — MAREA's ALL DRUMS and KIT both peak
at +0.2 dBFS — so a loop cut from them arrives at full scale, which `grade` flags as
clipping and which SuperDirt cannot use: amp = gain^4, so `# gain 1.2` is +7.6 dB and
the orbit clips the moment you touch it (`reference_superdirt_gain_is_quartic`). The
house rule is peaks at -6 dBFS at the source, and let the amplification happen later
(`feedback_gig_gain_staging`).
Two things this deliberately does NOT do:
* **Not per file.** One scalar for the kit. Normalising each file individually would
make a sparse hat loop as loud as a full drum bus and destroy the balance you would
otherwise get for free when two loops from the same kit play together.
* **Never boost.** A kit already quieter than the ceiling is left exactly where it is.
A quiet stem is quiet on purpose; lifting it would amplify its noise floor and throw
away real information about the arrangement.
"""
if not peaks_dbfs:
return 1.0
hottest = max(peaks_dbfs)
if hottest <= ceiling_db:
return 1.0
return float(10 ** ((ceiling_db - hottest) / 20.0))
def _drop_duplicates(items: list, thresh: float | None = None) -> list:
"""Remove near-identical cuts from a kit, keeping the best-graded of each group.
A producer pack overlaps its stems on purpose — ALL DRUMS, KIT, MAIN DRUM LOOP and
DRUM BREAKS are four views of one groove, and the finder will happily pick the same
bar out of all four. Eight slots holding three distinct sounds is a worse kit than
three. `kitcheck` reports duplicates after the fact; this stops them being written,
reusing the same tempo-agnostic mel fingerprint so the two agree by construction.
"""
from .kitcheck import DUPE_CORR, _fingerprint
thresh = DUPE_CORR if thresh is None else thresh # one definition of "same sound"
kept, prints = [], []
for it in items: # already sorted best-first
clip = it[4]
fp = _fingerprint(clip.mean(axis=1) if clip.ndim > 1 else clip, ANALYSIS_SR)
if any(float(fp @ q) > thresh for q in prints):
continue
kept.append(it); prints.append(fp)
return kept
def tag_with_clap(cuts: list[CutLoop], *, progress=None) -> None:
"""Attach CLAP multi-axis tags in place. Optional — a failure never blocks a cut.
The cheap probe in `roles.py` cannot tell a sung line from a rhodes chord; CLAP
can, and it is what turns a kit listing into something searchable by vibe rather
than by the producer's shorthand. It is advisory: `feedback_sample_identity` —
CLAP proposes, PLN's ear confirms.
"""
import sys
tide = Path.home() / "Work/Sound/Tidal/armada/tide-table"
if not tide.is_dir() or not cuts:
return
sys.path.insert(0, str(tide))
try:
import sample_semantics as SEM
except Exception as e:
if progress:
progress(f" (CLAP unavailable: {e})")
return
arrays = []
for c in cuts:
y, sr = sf.read(c.path, always_2d=True, dtype="float32")
arrays.append(y.mean(axis=1))
try:
emb = np.asarray(SEM.embed_audio_arrays(arrays))
for c, e in zip(cuts, emb):
c.tags = SEM.tag_audio_embed(e, topk=2)
except Exception as e:
if progress:
progress(f" (CLAP tagging failed: {e})")
# ── report ───────────────────────────────────────────────────────────────────
def cheatsheet(cuts: list[CutLoop]) -> str:
"""A paste-ready `.tidal` block: every kit, every index, what it is, how to loop it.
A kit you cannot address is a kit you will not use. Tidal indexes `n` by the
sorted filename, which is exactly the `NN_` order written above — so the index
printed here is the index that plays.
"""
lines = ["-- fred kits — generated by engine.stempack; n indices are sort order", ""]
for kit in sorted({c.kit for c in cuts}):
items = sorted((c for c in cuts if c.kit == kit), key=lambda c: c.name)
# NOT rounded to an integer. This is the number someone types into `setcps`,
# and ANGIE is 132.5 — printing "133 bpm" over loops cut at 132.5 makes every
# one of them play 0.4% fast, because `loopAt` is exact.
bpms = {round(c.bpm, 2) for c in items if c.bpm}
lines.append(f"-- {kit} ({len(items)} samples"
+ (f", {'/'.join(f'{b:g}' for b in sorted(bpms))} bpm)" if bpms else ")"))
for i, c in enumerate(items):
inst = (c.tags.get("instrument") or [("", 0)])[0][0]
how = f"loopAt {c.bars}" if c.bars > 0 else f"one-shot {c.dur_s:.2f}s"
lines.append(f"-- n {i:<2} {c.name:26.26} {c.tier} {how:12.12} "
f"{c.stem_role}{(' · ' + inst) if inst else ''}")
ex = items[0]
if ex.bars > 0:
# `loopAt` squeezes the file into N bars of the CURRENT cps, so the cps is
# half the instruction. Printing the bpm and leaving the arithmetic to the
# reader is how a 132.5 kit ends up played at 133.
if ex.bpm:
lines.append(f"-- setcps ({ex.bpm / 60 / 4:.4f}) -- {ex.bpm:g} bpm")
lines.append(f'-- d1 $ loopAt {ex.bars} $ chop 8 $ s "{kit}" # n 0')
else:
lines.append(f'-- d1 $ s "{kit}*4" # n (irand {len(items)})')
lines.append("")
return "\n".join(lines)
def track_summary(r: dict) -> dict:
"""The JSON-safe half of an analysis, so a child process can hand it to the parent.
The parent regenerates the whole report after CLAP tagging; concatenating the
children's own report fragments would freeze the tables before the tags exist.
"""
return {
"track": r["dir"].name,
"n_stems": len(r["stems"]),
"usable": [s["name"].role_token for s in r["stems"] if s["probe"].usable],
"skipped": [s["name"].role_token for s in r["stems"] if not s["probe"].usable],
"disagreements": [(s["name"].role_token, s["name"].claimed_family,
s["probe"].measured_family)
for s in r["stems"] if not s["probe"].agrees],
"grid_bpm": r["grid_bpm"], "grid_from": r["grid_from"],
"tempo_conf": r.get("tempo_conf", 0.0), "bar_ok": r.get("bar_ok", True),
"declared_bpm": r["declared_bpm"],
}
# CLAP phrases → the family they imply. Keyword matching, not an exhaustive map: the
# point is only to notice a loud disagreement, never to reassign anything.
_CLAP_FAMILY_HINTS = (
("drums", ("drum", "hi-hat", "hihat", "percussion", "snare", "kick", "clap",
"cymbal", "tom", "break")),
("bass", ("bass", "sub-bass", "808")),
("vox", ("voice", "singing", "vocal", "choir", "chant", "rap", "speech")),
)
def clap_family(tags: dict) -> Optional[str]:
"""The family CLAP's top instrument tag implies, or None if it says nothing useful."""
inst = (tags or {}).get("instrument") or []
if not inst:
return None
phrase = str(inst[0][0]).lower()
for fam, keys in _CLAP_FAMILY_HINTS:
if any(k in phrase for k in keys):
return fam
return "tonal"
def clap_disagreements(cuts: list[CutLoop]) -> list[tuple]:
"""Cuts whose CLAP tag contradicts the kit they landed in. Advisory, never applied.
The cheap probe cannot separate a sung line from a rhodes chord, and for a stem whose
filename token we do not recognise there is no producer label to fall back on either —
so those land wherever the coarse measurement pointed. CLAP is the one thing here that
can tell the difference, and it runs after the cut, so the honest use of it is to
surface the cases worth a second listen rather than to silently re-file them
(`feedback_sample_identity`: CLAP proposes, PLN's ear confirms).
"""
out = []
for c in cuts:
cf = clap_family(c.tags)
if not cf:
continue
# compare at the STEM level, not the family: a `hits` kit IS percussion, so CLAP
# hearing "a kick drum" in it agrees. And `fx`/`tonal`/`vox` all map to `other`
# and are genuinely not separable, so they never contradict each other.
want = R.FAMILIES.get(c.family, {}).get("stem")
got = R.FAMILIES.get(cf, {}).get("stem")
if want == got:
continue
soft = {"fx", "tonal", "vox"}
if c.family in soft and cf in soft:
continue
inst = c.tags["instrument"][0]
out.append((c.kit, c.name, c.stem_role, c.family, cf, inst[0], inst[1]))
return out
def report_md(results: list[dict], cuts: list[CutLoop]) -> str:
from collections import Counter
lines = ["# Fred stem-pack cut — report", ""]
for r in results:
lines += [f"## {r['track']}", "",
f"- stems: {r['n_stems']} ({len(r['usable'])} usable"
+ (f", skipped near-silent: {', '.join(r['skipped'])}" if r["skipped"] else "") + ")",
f"- grid: **{r['grid_bpm']:.1f} bpm** from `{r['grid_from']}` "
f"· confidence {r['tempo_conf']:.0%}"
+ ("" if r["bar_ok"] else " · **no usable pulse — chops only**")
+ (f" · producer declared **{r['declared_bpm']:.0f}**"
f" ({abs(r['grid_bpm'] - r['declared_bpm']) / r['declared_bpm'] * 100:.1f}% off)"
if r["declared_bpm"] else " · no declared tempo to check against")]
if r["disagreements"]:
lines.append("- label vs measurement disagreements (measurement wins):")
lines += [f" - `{tok}` claimed *{c}*, measured **{m}**"
for tok, c, m in r["disagreements"]]
mine = [c for c in cuts if c.track == r["track"]]
tiers = Counter(c.tier for c in mine)
lines += ["", f"- shipped {len(mine)} samples · tiers "
+ " ".join(f"{t}×{tiers[t]}" for t in reversed(TIER_ORDER) if tiers[t]), ""]
for kit in sorted({c.kit for c in mine}):
ks = sorted((c for c in mine if c.kit == kit), key=lambda c: c.name)
lines.append(f"### `{kit}` — {len(ks)}")
lines.append("")
lines.append("| n | file | tier | grade | bars | bpm | dur | from | CLAP |")
lines.append("|--:|---|---|--:|--:|--:|--:|---|---|")
for i, c in enumerate(ks):
tg = "; ".join(f"{v[0][0]}" for k, v in c.tags.items()
if v and k in ("instrument", "timbre")) if c.tags else ""
lines.append(
f"| {i} | `{c.name}` | {c.tier} | {c.grade:.3f} | "
f"{c.bars or '—'} | {c.bpm:.0f} | {c.dur_s:.2f}s | "
f"`{c.stem_role}` | {tg} |")
lines.append("")
dis = clap_disagreements(cuts)
if dis:
lines += ["## Worth a second listen", "",
"CLAP's instrument tag contradicts the kit these landed in. Nothing is "
"re-filed on this basis — CLAP is the one signal here that can separate "
"a sung line from a keyboard, so it is used to say what to audition "
"first, not to decide.", "",
"| kit | file | from stem | filed as | CLAP hears | conf |",
"|---|---|---|---|---|--:|"]
for kit, name, role, fam, cf, phrase, conf in dis:
lines.append(f"| `{kit}` | `{name}` | `{role}` | {fam} | {cf} "
f"({phrase}) | {conf:.2f} |")
lines.append("")
return "\n".join(lines)
def _run_track(track_dir: Path, opts: dict) -> tuple[dict, list, str]:
"""Analyze + export one track. Runs in a worker process, so it returns text.
Progress is buffered and returned rather than printed: interleaving nine tracks'
per-stem lines across processes produces a log nobody can read. One block per
track, printed when it lands.
"""
buf: list[str] = [f"\u25b6 {track_dir.name}"]
say = buf.append
r = analyze_track(track_dir, per_stem=opts["per_stem"], keep_per_stem=opts["keep"],
bars=opts["bars"], progress=say)
cuts: list[CutLoop] = []
if not opts["dry_run"]:
cuts = export_track(r, prefix=opts["prefix"], samples_root=opts["samples_root"],
min_tier=opts["min_tier"], link=opts["link"], progress=say)
# drop the audio before this crosses a process boundary
r["stems"] = [{k: v for k, v in s.items() if k not in ("st", "mono", "picks")}
for s in r["stems"]]
r["usable"] = [s for s in r["stems"] if s["probe"].usable]
return r, cuts, "\n".join(buf)
# ── CLI ──────────────────────────────────────────────────────────────────────
def main(argv=None) -> int:
import argparse
ap = argparse.ArgumentParser(prog="stempack", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("pack", type=Path, nargs="?",
help="folder of per-track stem folders (omit with --report-only)")
ap.add_argument("--tracks", nargs="*", default=None, help="track folders (default: all)")
ap.add_argument("--prefix", default="fred", help="kit name prefix")
ap.add_argument("--per-stem", type=int, default=6, help="candidates generated per stem")
ap.add_argument("--keep", type=int, default=3, help="candidates kept per stem")
ap.add_argument("--bars", type=int, nargs="+", default=[1, 2, 4, 8])
ap.add_argument("--min-tier", default=MIN_TIER, choices=TIER_ORDER)
ap.add_argument("--samples-root", type=Path, default=None)
ap.add_argument("--out", type=Path, default=None, help="write report/cheatsheet here")
ap.add_argument("--no-clap", action="store_true")
ap.add_argument("--no-link", action="store_true")
ap.add_argument("--dry-run", action="store_true", help="analyze and report, write nothing")
ap.add_argument("--report-only", action="store_true",
help="rebuild the report/cheatsheet from an existing --out, no cutting")
ap.add_argument("-j", "--jobs", type=int, default=_default_jobs(),
help="tracks analyzed in parallel (each pins one core)")
a = ap.parse_args(argv)
if a.report_only:
return _report_only(a.out or Path.cwd())
names = a.tracks or sorted(d.name for d in a.pack.iterdir() if d.is_dir())
todo = []
for nm in names:
d = a.pack / nm
if d.is_dir():
todo.append(d)
else:
print(f"! no such track folder: {d}", flush=True)
opts = dict(per_stem=a.per_stem, keep=a.keep, bars=tuple(a.bars), prefix=a.prefix,
samples_root=a.samples_root, min_tier=a.min_tier,
link=not a.no_link, dry_run=a.dry_run)
out = a.out or Path.cwd()
out.mkdir(parents=True, exist_ok=True)
results, cuts = [], []
if a.jobs == 1 or len(todo) == 1:
for d in todo:
r, c, log = _run_track(d, opts)
print(log, flush=True)
results.append(track_summary(r)); cuts += c
(out / "tracks.json").write_text(json.dumps(results, indent=2))
else:
# A track is the unit of parallelism: every stem in a track is cut against one
# shared beat grid, so stems cannot be split across workers, and tracks are
# fully independent. Fan out as SUBPROCESSES rather than a ProcessPoolExecutor
# — the pool's forkserver is broken under this Python (BrokenProcessPool on a
# trivial task), and one process per track also means a track that blows up
# loses only itself.
results, cuts = _fan_out(todo, a, out)
if cuts and not a.no_clap:
say = lambda m: print(m, flush=True) # noqa: E731
say("\nCLAP tagging…")
tag_with_clap(cuts, progress=say) # once, in the parent: one model load
(out / "fred_kits.md").write_text(report_md(results, cuts))
(out / "fred_kits.tidal").write_text(cheatsheet(cuts))
(out / "fred_kits.json").write_text(json.dumps([asdict(c) for c in cuts], indent=2))
print(f"\n{len(cuts)} samples in {len({c.kit for c in cuts})} kits "
f"\u2192 {out}/fred_kits.{{md,tidal,json}}", flush=True)
return 0
def _report_only(out: Path) -> int:
"""Rebuild `fred_kits.{md,tidal}` from the manifest already in `out`.
The manifest is the record; the report and the cheatsheet are views of it. Anything
that legitimately changes the audio after a cut — `restage.py` staging a kit's level
and re-grading it — leaves those views describing numbers that are no longer true, and
re-cutting an entire pack to refresh a markdown table would be absurd. This also picks
up report sections added since the cut was made.
"""
cuts_p = out / "fred_kits.json"
if not cuts_p.exists():
raise SystemExit(f"no manifest at {cuts_p}")
cuts = [CutLoop(**c) for c in json.loads(cuts_p.read_text())]
results = []
tj = out / "tracks.json"
if tj.exists():
results = json.loads(tj.read_text())
else: # fan-out runs leave them per child
for f in sorted((out / "_tracks").glob("*/tracks.json")):
results += json.loads(f.read_text())
(out / "fred_kits.md").write_text(report_md(results, cuts))
(out / "fred_kits.tidal").write_text(cheatsheet(cuts))
print(f"rebuilt report + cheatsheet for {len(cuts)} samples in "
f"{len({c.kit for c in cuts})} kits from {cuts_p.name}")
return 0
def run_at_idle_priority() -> None:
"""Drop this process to the lowest priority the scheduler has.
Passed to `subprocess` as a preexec hook, so it runs in the child between fork and
exec. SCHED_IDLE means "only when nothing else wants the CPU"; nice 19 is the
fallback when the policy call is not permitted.
"""
import os
os.nice(19)
try:
os.sched_setscheduler(0, os.SCHED_IDLE, os.sched_param(0))
except (AttributeError, OSError, PermissionError):
pass
def _default_jobs() -> int:
"""Half the cores, and never more than 6.
One child per track is tempting and wrong: it is the total that matters, and the
machine this runs on is the machine PLN performs and edits on. Half leaves the
other half for the desktop; the cap keeps a bigger box from turning a background
job into a 24-way memory hog (each child peaks around 2 GB).
"""
import os
n = len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else (os.cpu_count() or 4)
return max(1, min(6, n // 2))
def _fan_out(todo: list[Path], a, out: Path) -> tuple[list, list]:
"""Run one `--jobs 1` subprocess per track, then merge their results.
Children are told `--no-clap`: tagging loads a ~500 MB model, so it happens once
in the parent over the merged set rather than once per track.
They also run at **nice 19 and idle I/O**. This is not politeness — PLN works on
this machine while a cut is running, and nine children at 88% of a core each took
the load average to 15.6 on 16 cores and made the desktop unusable. Niced, the same
nine saturate the same sixteen cores and the editor still wins every scheduling
decision, because the work is genuinely background: nothing waits on it but me.
"""
import subprocess
from concurrent.futures import ThreadPoolExecutor
scratch = out / "_tracks"
scratch.mkdir(parents=True, exist_ok=True)
def run(d: Path):
sub = scratch / _tidal_token(d.name)
cmd = [__import__("sys").executable, "-u", "-m", "engine.stempack", str(d.parent),
"--tracks", d.name, "--jobs", "1", "--prefix", a.prefix,
"--per-stem", str(a.per_stem), "--keep", str(a.keep),
"--bars", *[str(b) for b in a.bars], "--min-tier", a.min_tier,
"--out", str(sub), "--no-clap"]
if a.samples_root:
cmd += ["--samples-root", str(a.samples_root)]
if a.no_link:
cmd.append("--no-link")
if a.dry_run:
cmd.append("--dry-run")
# tee to a per-track log rather than only capturing: a 30-minute analysis whose
# output only appears at exit is a run you cannot supervise.
sub.mkdir(parents=True, exist_ok=True)
with open(sub / "run.log", "w") as lf:
r = subprocess.run(cmd, cwd=Path(__file__).resolve().parent.parent,
stdout=lf, stderr=subprocess.STDOUT, text=True,
preexec_fn=run_at_idle_priority)
return d, sub, r
results, cuts = [], []
with ThreadPoolExecutor(max_workers=a.jobs) as ex: # threads only wait on children
for d, sub, r in ex.map(run, todo):
log = (sub / "run.log").read_text() if (sub / "run.log").exists() else ""
print(log.rstrip(), flush=True)
if r.returncode != 0:
print(f"! {d.name} failed (exit {r.returncode})", flush=True)
continue
j = sub / "fred_kits.json"
if j.exists():
cuts += [CutLoop(**c) for c in json.loads(j.read_text())]
s = sub / "tracks.json"
if s.exists():
results += json.loads(s.read_text())
return results, cuts
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""kitgate — the release gate for a freshly cut sample pack. Run before linking.
Cutting a pack produces audio; this decides whether it is fit to load into a running
rig. Five checks, four of them things a single earlier stage cannot answer alone:
1. **grade, on the shipped bytes.** `engine.grade` re-run over the written files, not
the candidate windows the finder scored. It is a separate code path reaching the same
verdict, so a disagreement means one of them is wrong — which is the point of running
it twice (`feedback_assembling_checks_revalidates`).
2. **kitcheck.** Dead bars, bar-multiple exactness, per-family periodicity, duplicates.
3. **Bank shadowing.** `loadSoundFiles` defaults to appendToExisting = false, so a new
folder whose basename matches an existing bank frees that bank's buffers and takes
the name — silently, with the only symptom being that an old track now plays the
wrong sound. Checked BEFORE anything is linked, via `tools/pvbanks.py` so this agrees
with `bank-check.py` by construction (`feedback_one_parser_per_concept`).
4. **`n` indices.** The cheat sheet numbers `n` from the manifest; SuperDirt numbers it
from the folder. Export never empties a kit dir, so a re-cut can leave a stale file
behind and shift every index after it. Compared listing-to-listing, index for index.
5. **Rights.** A kit cut from someone else's stems must not be releasable per-track by
default. Reported here, ledgered by `armada/tide-table/register_fred_rights.py`.
python3 kitgate.py <cuts.json> [--link] [--source "..."]
`--link` performs the Dirt-Samples symlink for every kit, but only if 1-4 pass.\n`--prune` first deletes kit-dir files the manifest does not list.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from collections import Counter
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
TIDAL_TOOLS = HERE.parent # tools/
# A DEFECT makes the file unusable and blocks. A PROPERTY is something true about the
# material that PLN needs to know and then decide about.
#
# Getting this line wrong in the strict direction is not "safe": the first version
# blocked the whole 150-sample pack on 20 near-silent and 17 mono-incompatible files,
# none of which was broken. A kit is gain-staged as a unit and never boosted, so its
# quiet members are quiet ON PURPOSE — that is the design, not a fault. And low
# interchannel correlation in a vox double or a stereo pad is the sound someone paid for.
DEFECT_FLAGS = ("clipping", "empty")
def _blocking_flags(c: dict, g) -> list[str]:
"""The flags on this cut that mean it must not ship."""
out = [f for f in g.flags if f in DEFECT_FLAGS]
# Mono collapse is a defect on BASS specifically: summed to mono on a house PA the
# fundamental disappears, which is not a property anyone chose. On a vox double or a
# pad it is the width they paid for. ([[floor_response_vs_measurement]])
if c.get("family") == "bass" and "mono-incompatible" in g.flags:
out.append("mono-incompatible (bass — the fundamental dies in mono)")
return out
def check_grades(cuts: list[dict]) -> tuple[bool, list[str]]:
"""Re-grade the written files and compare with what the finder recorded.
Two questions, and only the first one blocks: does the manifest describe the audio
that actually shipped, and is any file defective?
"""
from engine import grade as G
lines, bad, notes = [], [], []
tiers = Counter()
for c in cuts:
g = G.grade(c["path"], role=c.get("family"))
tiers[g.tier] += 1
# the finder graded the same audio in memory; a real gap means one path is wrong
if abs(g.grade - c["grade"]) > 0.02:
bad.append(f" ✗ {c['kit']}/{c['name']}: finder said {c['grade']:.3f}, "
f"file grades {g.grade:.3f}")
for f in _blocking_flags(c, g):
bad.append(f" ✗ {c['kit']}/{c['name']}: {f}")
for f in g.flags:
if f in DEFECT_FLAGS or f == "one-shot":
continue
# `off-grid` on something the manifest calls a chop is noise: a sub-bar
# one-shot has no bar grid to be off. grade guesses from duration; the
# manifest KNOWS, so the manifest wins.
if f == "off-grid" and not c.get("bars"):
continue
if f == "mono-incompatible" and c.get("family") == "bass":
continue # already counted as blocking
notes.append((f, c, g))
lines.append(" tiers " + " ".join(f"{t}x{tiers[t]}" for t in "SABCD" if tiers[t])
+ f" · {len(cuts)} files, manifest matches the audio"
+ (f" · {len({id(c) for _, c, _ in notes})} worth a second listen"
if notes else ""))
lines += bad[:20]
if len(bad) > 20:
lines.append(f" … and {len(bad) - 20} more")
lines += _advisory(notes)
return not bad, lines
def _advisory(notes: list[tuple]) -> list[str]:
"""The properties, grouped, with the numbers that let PLN judge them by eye.
Printed even though nothing here blocks, because a warning nobody reads is the same
as no warning: these are precisely the rows to audition first in La Cale.
"""
if not notes:
return []
by_flag: dict[str, list[tuple]] = {}
for f, c, g in notes:
by_flag.setdefault(f, []).append((c, g))
out = []
for f, items in sorted(by_flag.items(), key=lambda kv: -len(kv[1])):
out.append(f" · {f} ×{len(items)} (advisory — a property, not a defect)")
for c, g in items[:4]:
m = g.metrics
detail = (f"peak {m['peak_dbfs']:.1f} rms {m['rms_dbfs']:.1f}"
if f == "near-silent" else
f"corr {m['interchannel_corr']:+.2f}" if f == "mono-incompatible" else
f"seam {m['seam_click_db']:+.1f} dB")
out.append(f" {c['kit']}/{c['name']} {g.tier} {detail}")
if len(items) > 4:
out.append(f" … and {len(items) - 4} more")
return out
def check_kits(cuts: list[dict]) -> tuple[bool, list[str]]:
from engine import kitcheck as K
checks = K.check_cuts(cuts)
hard = [c for c in checks if not c.ok]
soft = [c for c in checks if c.ok and c.problems]
dupes = [c for c in checks if c.dupe_of]
lines = [f" {len(checks) - len(hard) - len(soft)} clean, {len(soft)} advisory, "
f"{len(hard)} failing · {len(checks) - len(dupes)} distinct sounds"]
lines += [f" ✗ {c.kit}/{c.name}: {'; '.join(c.problems)}" for c in hard[:20]]
return not hard, lines
def check_shadowing(cuts: list[dict]) -> tuple[bool, list[str]]:
"""Would linking these kits steal a name an existing bank already answers to?"""
sys.path.insert(0, str(TIDAL_TOOLS))
try:
from pvbanks import scan
except Exception as e:
return True, [f" (skipped: {e})"]
existing = scan()
kits = sorted({c["kit"] for c in cuts})
clash = [k for k in kits if k in existing]
lines = [f" {len(kits)} kits vs {len(existing)} known banks"]
lines += [f" ✗ {k} already exists: "
f"{', '.join(str(e[0]) for e in existing[k])}" for k in clash]
return not clash, lines
def check_indices(cuts: list[dict]) -> tuple[bool, list[str]]:
"""Does `n <i>` on disk address the file the cheat sheet says it does?
The cheat sheet numbers `n` by enumerating the manifest sorted by name, and
SuperDirt numbers it by enumerating the FOLDER sorted by name. Those two agree
only as long as the folder holds exactly the manifest's files — and export writes
into an existing kit dir without ever emptying it. So a re-cut that renames one
file (`00_kick_2b.wav` → `00_kick_4b.wav`) leaves both behind, and every index
from that point on addresses the wrong sound, silently, with a cheat sheet that
looks right. Compare the two listings index for index, not as sets.
Uses `pvbanks.playable` so "what SuperDirt will load" has one definition here,
in `bank-check.py`, and in the watcher.
"""
sys.path.insert(0, str(TIDAL_TOOLS))
try:
from pvbanks import playable
except Exception as e:
return True, [f" (skipped: {e})"]
by_kit: dict[str, list[dict]] = {}
for c in cuts:
by_kit.setdefault(c["kit"], []).append(c)
bad, orphans = [], 0
for kit, items in sorted(by_kit.items()):
kit_dir = Path(items[0]["path"]).parent
want = [c["name"] for c in sorted(items, key=lambda c: c["name"])]
have = [p.stem for p in playable(kit_dir)]
extra = [h for h in have if h not in want]
orphans += len(extra)
missing = [w for w in want if w not in have]
shift = next((i for i, (w, h) in enumerate(zip(want, have)) if w != h), None)
if extra or missing:
bad.append(f" ✗ {kit}: {len(extra)} orphan, {len(missing)} missing"
+ (f" — n {shift} onward addresses the wrong file" if shift is not None
else " — indices still line up")
+ (f" · orphans: {', '.join(extra[:4])}" if extra else ""))
lines = [f" {len(by_kit)} kit dirs hold exactly their {len(cuts)} manifest files"
if not bad else f" {orphans} orphan files across {len(bad)} kits"]
lines += bad[:20]
if orphans:
lines.append(" → re-run with --prune to delete files the manifest does not list")
return not bad, lines
def check_rights(cuts: list[dict], source: str) -> tuple[bool, list[str]]:
"""Are these banks recorded as NOT releasable per-track? Advisory, never blocking."""
ledger = (TIDAL_TOOLS.parent / "armada/tide-table/rights_ledger.json")
kits = sorted({c["kit"] for c in cuts})
if not ledger.exists():
return True, [" (no ledger)"]
banks = json.loads(ledger.read_text()).get("banks", {})
unregistered = [k for k in kits if k not in banks]
releasable = [k for k in kits
if banks.get(k, {}).get("status") in ("original", "cleared", "dirt_samples")]
lines = [f" {len(kits) - len(unregistered)} of {len(kits)} in the ledger"]
if unregistered:
lines.append(f" ! {len(unregistered)} unregistered — run register_fred_rights.py "
f"so they block per-track release with their provenance attached")
if releasable:
lines.append(f" ! {len(releasable)} marked releasable per-track: "
f"{', '.join(releasable[:5])}")
return True, lines
def prune_orphans(cuts: list[dict]) -> int:
"""Delete kit-dir files the manifest does not list. Only ever inside a kit dir
named by the manifest, and only ever an audio file — we never delete what we did
not write."""
sys.path.insert(0, str(TIDAL_TOOLS))
from pvbanks import playable
keep: dict[Path, set[str]] = {}
for c in cuts:
keep.setdefault(Path(c["path"]).parent, set()).add(c["name"])
n = 0
for kit_dir, names in keep.items():
for f in playable(kit_dir):
if f.stem not in names:
f.unlink()
n += 1
return n
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="kitgate", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("cuts_json", type=Path)
ap.add_argument("--link", action="store_true", help="symlink into Dirt-Samples if clean")
ap.add_argument("--source", default="third-party producer stem pack")
ap.add_argument("--prune", action="store_true",
help="delete files in the kit dirs that the manifest does not list "
"(leftovers from an earlier cut) before checking")
a = ap.parse_args(argv)
cuts = json.loads(a.cuts_json.read_text())
if a.prune:
print(f"pruned {prune_orphans(cuts)} orphan files\n")
print(f"kitgate — {len(cuts)} samples in {len({c['kit'] for c in cuts})} kits\n")
results = {}
for label, fn in [("grade (re-run on the written files)", lambda: check_grades(cuts)),
("kitcheck", lambda: check_kits(cuts)),
("bank shadowing", lambda: check_shadowing(cuts)),
("n indices (folder vs manifest)", lambda: check_indices(cuts)),
("rights", lambda: check_rights(cuts, a.source))]:
ok, lines = fn()
results[label] = ok
print(f"{'✓' if ok else '✗'} {label}")
for l in lines:
print(l)
print()
blocking = [k for k, v in results.items() if not v and k != "rights"]
if blocking:
print(f"BLOCKED by: {', '.join(blocking)}")
return 1
print("all blocking checks pass")
if a.link:
from engine import publish as P
acted = Counter()
# Where the audio is comes from the MANIFEST, not from `publish.samples_root()`.
# A pack cut with `--samples-root` is staged beside its source, so the default
# root would look in the wrong place and report every kit as missing — and the
# manifest is the one thing that already knows, per kit, exactly where its files
# were written.
roots = {c["kit"]: Path(c["path"]).parent.parent for c in cuts}
for kit in sorted(roots):
try:
acted[P.link_kit(kit, samples=roots[kit]).action] += 1
except Exception as e:
print(f" ! {kit}: {e}")
acted["failed"] += 1
print("linked into Dirt-Samples: " +
", ".join(f"{v} {k}" for k, v in sorted(acted.items())))
if acted.get("skipped-real-dir"):
print(" (a real directory was left untouched — we never delete what we did "
"not create)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""kitindex — build the sample-kit index the auditioner SPA browses.
One JSON describing every kit under the sample root: what is in it, how to loop it,
and a peak envelope small enough to draw without downloading the audio. Written to
`armada/ui/public/kits.json`, fetched by `kits.html`, and served by both
`vite.config.ts` and `armada/serve.py` through the `samples` mount — so the URLs in
here work identically in dev and on the LAN.
Two sources, merged, and the merge is the point:
* **The folder is the truth about what exists.** SuperDirt enumerates the directory,
so the index has to as well, via `pvbanks.playable` (the same listing kitgate and
bank-check use). A kit with no manifest — 163 of PLN's kits predate the Foundry —
still has to be browsable, or the tool only works on what it built itself.
* **A Foundry manifest is the truth about what a file MEANS.** tier, grade, family,
producer role, bars, bpm and CLAP tags all come from `fred_kits.json`-style cuts;
nothing here re-derives them, so the SPA and the gate cannot disagree.
`n` comes from the folder listing, never from the manifest order, because `n` is
what SuperDirt will play. (kitgate proves the two agree; this file does not assume it.)
Kit dirs do not all sit at the same depth — a pack can be staged in its own subtree
(`Samples/Fred/output/fred_marea_drums/`) while the older kits sit directly under
`Samples/`. So `--root` is repeatable and says *where to look for kit folders*, while
`--mount-root` is the one directory the SPA's `samples` mount points at and the only
thing URLs are computed from. Keeping those two ideas apart is what lets a pack move
without every URL in the index changing.
python3 kitindex.py --root ~/Work/Sound/Samples/Fred/output --cuts <cuts.json>
python3 kitindex.py --all # every kit under the sample root
"""
from __future__ import annotations
import argparse
import fnmatch
import json
import sys
import time
from pathlib import Path
import numpy as np
import soundfile as sf
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent)) # tools/, for pvbanks
PEAKS = 128 # envelope columns per sample — enough for a 200 px row
OUT = HERE.parent.parent / "armada/ui/public/kits.json"
MOUNT = "samples" # the /audio/<prefix> mount in armada/ui/audio-mounts.json
CACHE = HERE / ".kitindex-cache.json" # envelope cache, keyed on size+mtime
def envelope(path: Path, cols: int = PEAKS) -> tuple[list[int], float, float, int, int]:
"""A tiny peak envelope, plus the facts the player needs before decoding.
Read as one block: these files are seconds long, and a per-column seek would be
slower than the read. Mono-summed — the SPA draws one lane per sample, and a
two-lane waveform at 200 px wide is noise.
"""
y, sr = sf.read(str(path), dtype="float32", always_2d=True)
n, ch = y.shape
mono = y.mean(axis=1)
if n == 0:
return [0] * cols, 0.0, 0.0, sr, ch
# pad to a whole number of columns so the last one is not a short-window artefact
per = max(1, int(np.ceil(n / cols)))
pad = per * cols - n
blocks = np.abs(np.pad(mono, (0, pad))).reshape(cols, per)
peak = float(np.max(np.abs(mono)))
env = blocks.max(axis=1)
scaled = (env / (peak + 1e-12) * 100.0).round().astype(int).tolist()
rms = float(np.sqrt(np.mean(mono ** 2)))
return scaled, peak, rms, sr, ch
def db(x: float) -> float:
return round(float(20 * np.log10(max(x, 1e-12))), 1)
def load_cuts(paths: list[Path]) -> dict[str, dict]:
"""Manifest rows keyed by absolute audio path."""
by_path: dict[str, dict] = {}
for p in paths:
for c in json.loads(p.read_text()):
by_path[str(Path(c["path"]).resolve())] = c
return by_path
def _load_cache() -> dict:
"""Envelopes keyed on `path|size|mtime_ns`.
Reading 1884 files to draw 1884 little waveforms takes 3.5 minutes, and the whole
point of re-running this is that ONE pack changed. The key includes size and mtime
so a restaged kit — same path, new bytes — misses the cache: a stale envelope drawn
over fresh audio would be a lie that looks like data.
"""
try:
return json.loads(CACHE.read_text())
except Exception:
return {}
def build(roots: list[Path], mount_root: Path, cuts: dict[str, dict],
only: list[str], *, use_cache: bool = True, progress=None) -> dict:
from pvbanks import playable
cache = _load_cache() if use_cache else {}
fresh: dict[str, list] = {}
hits = 0
def url_for(f: Path) -> str:
"""`/audio/samples/<path under the mount>`. A kit outside the mount would be
unplayable in the browser, so say so here rather than serve a 404."""
try:
return f"/audio/{MOUNT}/{f.resolve().relative_to(mount_root.resolve())}"
except ValueError:
return ""
kits = []
seen: set[str] = set()
dirs = [x for r in roots if r.is_dir() for x in sorted(r.iterdir()) if x.is_dir()]
for d in dirs:
if only and not any(fnmatch.fnmatch(d.name, pat) for pat in only):
continue
files = playable(d)
if not files:
continue
if d.name in seen:
# Two dirs claiming one bank name is exactly the shadowing SuperDirt does
# silently; the index must not pick a winner behind PLN's back.
if progress:
progress(f" ! {d.name}: a second directory claims this bank name — skipped")
continue
seen.add(d.name)
samples = []
for n, f in enumerate(files): # n IS the folder index SuperDirt uses
c = cuts.get(str(f.resolve()), {})
st = f.stat()
key = f"{f}|{st.st_size}|{st.st_mtime_ns}"
cached = cache.get(key)
if cached:
env, peak, rms, sr, ch = cached
hits += 1
else:
try:
env, peak, rms, sr, ch = envelope(f)
except Exception as e: # a kit with one unreadable file stays usable
if progress:
progress(f" ! {f.name}: {e}")
continue
fresh[key] = [env, peak, rms, sr, ch]
samples.append({
"n": n, "name": f.stem, "file": f.name,
"url": url_for(f),
"sr": sr, "ch": ch, "peak_dbfs": db(peak), "rms_dbfs": db(rms),
"env": env,
# from the manifest when the Foundry cut it, absent otherwise
"bars": c.get("bars"), "bpm": c.get("bpm"),
# where in the source track this was cut from: three loops off one stem
# are otherwise indistinguishable in a list, both named for the stem
"start_s": c.get("start_s"),
"dur_s": c.get("dur_s"), "tier": c.get("tier"),
"grade": c.get("grade"), "family": c.get("family"),
"stem_role": c.get("stem_role"), "track": c.get("track"),
"mode": c.get("mode"), "flags": c.get("flags") or [],
"tags": c.get("tags") or {},
})
if not samples:
continue
known = [s for s in samples if s["bpm"]]
kits.append({
"kit": d.name, "n_samples": len(samples),
"source": "foundry" if known else "legacy",
"track": next((s["track"] for s in samples if s["track"]), None),
"family": next((s["family"] for s in samples if s["family"]), None),
"bpm": known[0]["bpm"] if known else None,
"samples": samples,
})
if progress:
progress(f" {d.name:38.38} {len(samples):3} samples")
if use_cache:
try:
CACHE.write_text(json.dumps(fresh, separators=(",", ":")))
except OSError:
pass
if progress and hits:
progress(f" ({hits} envelopes from cache)")
return {"generated": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"roots": [str(r) for r in roots], "mount_root": str(mount_root),
"peaks": PEAKS, "kits": kits}
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="kitindex", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--cuts", type=Path, action="append", default=[],
help="a stempack cuts manifest; repeatable")
ap.add_argument("--only", action="append", default=[],
help="glob on kit name; repeatable (default: every kit)")
ap.add_argument("--all", action="store_true", help="explicit form of no --only")
ap.add_argument("--root", type=Path, action="append", default=[],
help="a directory holding kit folders; repeatable "
"(default: the sample root itself)")
ap.add_argument("--mount-root", type=Path, default=None,
help="what /audio/samples points at (default: the sample root)")
ap.add_argument("--out", type=Path, default=OUT)
ap.add_argument("--no-cache", action="store_true",
help="re-read every file instead of reusing cached envelopes")
a = ap.parse_args(argv)
from engine import publish
mount_root = a.mount_root or publish.samples_root()
roots = a.root or [publish.samples_root()]
cuts = load_cuts(a.cuts)
print(f"kitindex — {', '.join(str(r) for r in roots)}"
+ (f", {len(cuts)} manifest rows from {len(a.cuts)} file(s)" if cuts else ""))
idx = build(roots, mount_root, cuts, [] if a.all else a.only,
use_cache=not a.no_cache, progress=print)
a.out.parent.mkdir(parents=True, exist_ok=True)
body = json.dumps(idx, separators=(",", ":"))
a.out.write_text(body)
# `npm run build` copies public/ into dist/, so a regenerated index would otherwise
# be invisible to the built page that `serve.py` hands the LAN — the same shape as
# the stale preload that debuted at a venue. Write both, or neither.
dist = a.out.parent.parent / "dist" / a.out.name
if dist.parent.is_dir():
dist.write_text(body)
print(f" (also updated {dist})")
n = sum(k["n_samples"] for k in idx["kits"])
legacy = sum(1 for k in idx["kits"] if k["source"] == "legacy")
unreachable = [k["kit"] for k in idx["kits"]
if any(not s["url"] for s in k["samples"])]
if unreachable:
print(f" ! {len(unreachable)} kits are outside {mount_root} and cannot be "
f"served: {', '.join(unreachable[:4])}")
print(f"\n{len(idx['kits'])} kits ({legacy} without a manifest), {n} samples "
f"→ {a.out} ({a.out.stat().st_size / 1024:.0f} KB)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
# packs/ — the record of what was cut
One directory per source pack, holding the **manifest and its views** for a cut that
actually shipped:
```
packs/<pack>/
fred_kits.json # the manifest — every sample, where it came from, its grade + tags
fred_kits.md # the report: per-track probe verdicts, tempo, per-kit tables
fred_kits.tidal # the cheat sheet — paste-ready, `n` index of every sample
kitcheck.md # the audit
```
The audio itself lives under a **staging root**, symlinked into Dirt-Samples, never
here — this is provenance, not a copy. Where that root is, is per-pack: kits cut from
someone else's stems are staged beside their source (`Samples/Fred/output/<kit>/`, via
`--samples-root`) so the pack stays one movable, deletable thing, while ParVagues' own
kits sit directly under `Samples/<kit>/`. Either way the bank name is the directory
name, so `s "<kit>"` does not care which. The manifest is the record; `.md` and
`.tidal` are views of it and can be rebuilt at any time with:
```bash
python3 -m engine.stempack --report-only --out packs/<pack>
```
Keeping the manifest is what makes a cut auditable months later: which stem a loop came
from, at what tempo, graded how, and what CLAP heard in it.
This source diff could not be displayed because it is too large. You can view the blob instead.
# Fred stem-pack cut — report
## ANGIE
- stems: 11 (11 usable)
- grid: **132.5 bpm** from `all drums` · confidence 88% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `bv` claimed *vox*, measured **drums**
- `fx` claimed *fx*, measured **drums**
- `kicks` claimed *hits*, measured **bass**
- shipped 22 samples · tiers S×11 A×2 B×9
### `fred_angie_bass` — 3
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_8b` | S | 0.991 | 8 | 133 | 14.49s | `bass` | an acid bassline; a fat thick sound |
| 1 | `01_bass_8b` | S | 0.984 | 8 | 133 | 14.49s | `bass` | an acid bassline; a fat thick sound |
| 2 | `02_bass_2b` | S | 0.920 | 2 | 133 | 3.62s | `bass` | an acid bassline; a punchy tight sound |
### `fred_angie_drums` — 3
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_drumbreaks_2b` | S | 0.934 | 2 | 133 | 3.62s | `drum breaks` | a drum break; a muddy boomy sound |
| 1 | `01_alldrums_1b` | S | 0.909 | 1 | 133 | 1.81s | `all drums` | a drum break; a warm sound |
| 2 | `02_alldrums_2b` | A | 0.869 | 2 | 133 | 3.62s | `all drums` | a drum break; a warm sound |
### `fred_angie_fx` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_fx_chop` | S | 0.953 | — | 133 | 0.56s | `fx` | a spoken voice; an airy breathy sound |
| 1 | `01_fx_1b` | A | 0.890 | 1 | 133 | 1.81s | `fx` | hand percussion; a dusty lo-fi sound |
### `fred_angie_hits` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_kicks_chop` | S | 1.000 | — | 133 | 0.45s | `kicks` | a kick drum; a punchy tight sound |
| 1 | `01_snare_chop` | S | 0.916 | — | 133 | 1.81s | `snare` | a hi-hat; a punchy tight sound |
### `fred_angie_tonal` — 5
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_synth_8b` | S | 0.935 | 8 | 133 | 14.49s | `synth` | a synth pad; a lush rich sound |
| 1 | `01_synth_2b` | B | 0.785 | 2 | 133 | 3.62s | `synth` | a synth pad; a lush rich sound |
| 2 | `02_synth_2b` | B | 0.764 | 2 | 133 | 3.62s | `synth` | an electric guitar; a lush rich sound |
| 3 | `03_padstuff_2b` | B | 0.740 | 2 | 133 | 3.62s | `pad stuff` | an electric guitar; a dark sound |
| 4 | `04_padstuff_1b` | B | 0.737 | 1 | 133 | 1.81s | `pad stuff` | a synth lead; a metallic sound |
### `fred_angie_vox` — 7
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bv_chop` | S | 0.996 | — | 133 | 0.67s | `bv` | a spoken voice; an airy breathy sound |
| 1 | `01_lv_chop` | S | 0.976 | — | 133 | 0.29s | `lv` | a saxophone; an airy breathy sound |
| 2 | `02_lv_4b` | B | 0.758 | 4 | 133 | 7.24s | `lv` | a solo singing voice; a dreamy washed-out sound |
| 3 | `03_lv_8b` | B | 0.740 | 8 | 133 | 14.49s | `lv` | a solo singing voice; a warm sound |
| 4 | `04_lv_2b` | B | 0.716 | 2 | 133 | 3.62s | `lv` | a solo singing voice; a dreamy washed-out sound |
| 5 | `05_bv_1b` | B | 0.710 | 1 | 133 | 1.81s | `bv` | a spoken voice; a dreamy washed-out sound |
| 6 | `06_bv_1b` | B | 0.704 | 1 | 133 | 1.81s | `bv` | a solo singing voice; a dreamy washed-out sound |
## ANGIE INTERLUDE
- stems: 7 (7 usable)
- grid: **112.3 bpm** from `noise` · confidence 58% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `kick` claimed *hits*, measured **bass**
- `noise` claimed *fx*, measured **drums**
- `pad` claimed *tonal*, measured **bass**
- shipped 8 samples · tiers S×6 A×2
### `fred_angieinterlude_bass` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_pad_2b` | S | 0.916 | 2 | 112 | 4.27s | `pad` | a synth pad; a lush rich sound |
### `fred_angieinterlude_fx` — 4
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_ipadtextures_chop` | S | 0.998 | — | 112 | 0.36s | `ipad textures` | a saxophone; a punchy tight sound |
| 1 | `01_drone_chop` | S | 0.986 | — | 112 | 0.34s | `drone` | a rhodes electric piano; a dark sound |
| 2 | `02_noise_4b` | A | 0.898 | 4 | 112 | 8.54s | `noise` | a sub bass; an airy breathy sound |
| 3 | `03_noise_1b` | A | 0.896 | 1 | 112 | 2.14s | `noise` | hand percussion; an airy breathy sound |
### `fred_angieinterlude_hits` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_kick_chop` | S | 0.991 | — | 112 | 0.55s | `kick` | a kick drum; a punchy tight sound |
### `fred_angieinterlude_vox` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_voices_chop` | S | 0.998 | — | 112 | 0.45s | `voices` | a spoken voice; a punchy tight sound |
| 1 | `01_fvox_chop` | S | 0.997 | — | 112 | 0.30s | `f vox` | a spoken voice; an airy breathy sound |
## BIG HEN
- stems: 10 (10 usable)
- grid: **132.5 bpm** from `all drums` · confidence 82% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `fx` claimed *fx*, measured **drums**
- `lv` claimed *vox*, measured **drums**
- `snare` claimed *hits*, measured **tonal**
- shipped 24 samples · tiers S×14 A×3 B×7
### `fred_bighen_bass` — 3
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_8b` | S | 0.985 | 8 | 133 | 14.49s | `bass` | a sub bass; a punchy tight sound |
| 1 | `01_bass_8b` | S | 0.983 | 8 | 133 | 14.49s | `bass` | a kick drum; a punchy tight sound |
| 2 | `02_bass_4b` | B | 0.735 | 4 | 133 | 7.24s | `bass` | a sub bass; a punchy tight sound |
### `fred_bighen_drums` — 9
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_maindrumloop_2b` | S | 0.999 | 2 | 133 | 3.62s | `main drum loop` | a drum break; a dark sound |
| 1 | `01_alldrums_2b` | S | 0.966 | 2 | 133 | 3.62s | `all drums` | a drum break; a warm sound |
| 2 | `02_drumbreaks_1b` | S | 0.902 | 1 | 133 | 1.81s | `drum breaks` | a drum break; a punchy tight sound |
| 3 | `03_maindrumloop_1b` | B | 0.750 | 1 | 133 | 1.81s | `main drum loop` | a drum break; a dark sound |
| 4 | `04_maindrumloop_8b` | B | 0.750 | 8 | 133 | 14.49s | `main drum loop` | a drum break; a muddy boomy sound |
| 5 | `05_alldrums_4b` | B | 0.749 | 4 | 133 | 7.24s | `all drums` | a drum break; a warm sound |
| 6 | `06_alldrums_8b` | B | 0.745 | 8 | 133 | 14.49s | `all drums` | a drum break; a muddy boomy sound |
| 7 | `07_drumbreaks_4b` | B | 0.719 | 4 | 133 | 7.24s | `drum breaks` | a drum break; a muddy boomy sound |
| 8 | `08_drumbreaks_8b` | B | 0.716 | 8 | 133 | 14.49s | `drum breaks` | a drum break; a muddy boomy sound |
### `fred_bighen_fx` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_fx_1b` | S | 0.911 | 1 | 133 | 1.81s | `fx` | hand percussion; a woody organic sound |
### `fred_bighen_hits` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_snare_chop` | A | 0.875 | — | 133 | 0.38s | `snare` | a hi-hat; a punchy tight sound |
### `fred_bighen_tonal` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_strings_2b` | S | 0.915 | 2 | 133 | 3.62s | `strings` | an electric guitar; a metallic sound |
| 1 | `01_synth_2b` | S | 0.913 | 2 | 133 | 3.62s | `synth` | a rhodes electric piano; a lush rich sound |
### `fred_bighen_vox` — 8
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_lv_chop` | S | 0.998 | — | 133 | 0.37s | `lv` | a solo singing voice; a dusty lo-fi sound |
| 1 | `01_lv_4b` | S | 0.934 | 4 | 133 | 7.24s | `lv` | a solo singing voice; a lush rich sound |
| 2 | `02_lv_1b` | S | 0.931 | 1 | 133 | 1.81s | `lv` | a solo singing voice; a dusty lo-fi sound |
| 3 | `03_lv_2b` | S | 0.923 | 2 | 133 | 3.62s | `lv` | a synth lead; a smooth clean sound |
| 4 | `04_bv_2b` | S | 0.914 | 2 | 133 | 3.62s | `bv` | an electric guitar; a dreamy washed-out sound |
| 5 | `05_bv_chop` | S | 0.907 | — | 133 | 1.78s | `bv` | an electric guitar; a dreamy washed-out sound |
| 6 | `06_bv_1b` | A | 0.896 | 1 | 133 | 1.81s | `bv` | a spoken voice; a dreamy washed-out sound |
| 7 | `07_bv_4b` | A | 0.858 | 4 | 133 | 7.24s | `bv` | a synth lead; a dreamy washed-out sound |
## DERMOT
- stems: 13 (13 usable)
- grid: **152.0 bpm** from `all drums` · confidence 92% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `keys1` claimed *tonal*, measured **drums**
- `kick` claimed *hits*, measured **bass**
- `snare` claimed *hits*, measured **fx**
- shipped 19 samples · tiers S×9 A×5 B×5
### `fred_dermot_bass` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_8b` | S | 0.948 | 8 | 152 | 12.63s | `bass` | an acid bassline; a muddy boomy sound |
| 1 | `01_bass_2b` | S | 0.931 | 2 | 152 | 3.16s | `bass` | a sub bass; a fat thick sound |
### `fred_dermot_drums` — 7
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_drumbreaks_1b` | S | 0.953 | 1 | 152 | 1.58s | `drum breaks` | a drum break; a warm sound |
| 1 | `01_drumbreaks_8b` | S | 0.951 | 8 | 152 | 12.63s | `drum breaks` | a drum break; a warm sound |
| 2 | `02_alldrums_1b` | S | 0.926 | 1 | 152 | 1.58s | `all drums` | a drum break; a warm sound |
| 3 | `03_alldrums_4b` | S | 0.926 | 4 | 152 | 6.32s | `all drums` | a drum break; a warm sound |
| 4 | `04_alldrums_2b` | A | 0.893 | 2 | 152 | 3.16s | `all drums` | a drum break; a warm sound |
| 5 | `05_hihatsetc_4b` | A | 0.867 | 4 | 152 | 6.32s | `hihats etc` | a hi-hat; a metallic sound |
| 6 | `06_hihatsetc_1b` | A | 0.864 | 1 | 152 | 1.58s | `hihats etc` | hand percussion; a bright sparkling sound |
### `fred_dermot_hits` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_snare_chop` | S | 0.975 | — | 152 | 0.51s | `snare` | a drum break; a punchy tight sound |
| 1 | `01_kick_chop` | S | 0.900 | — | 152 | 0.40s | `kick` | a kick drum; a punchy tight sound |
### `fred_dermot_tonal` — 3
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_keys1_2b` | A | 0.837 | 2 | 152 | 3.16s | `keys1` | a synth pad; a lush rich sound |
| 1 | `01_guitar_1b` | B | 0.794 | 1 | 152 | 1.58s | `guitar` | hand percussion; a punchy tight sound |
| 2 | `02_guitar_4b` | B | 0.720 | 4 | 152 | 6.32s | `guitar` | an acid bassline; a lush rich sound |
### `fred_dermot_vox` — 5
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_lv_chop` | S | 0.992 | — | 152 | 0.50s | `lv` | an electric guitar; a dreamy washed-out sound |
| 1 | `01_choir_chop` | A | 0.852 | — | 152 | 0.45s | `choir` | an electric guitar; a dreamy washed-out sound |
| 2 | `02_bv_chop` | B | 0.738 | — | 152 | 1.80s | `bv` | a saxophone; a dreamy washed-out sound |
| 3 | `03_lv_8b` | B | 0.706 | 8 | 152 | 12.63s | `lv` | a solo singing voice; a lush rich sound |
| 4 | `04_lv_2b` | B | 0.700 | 2 | 152 | 3.16s | `lv` | a solo singing voice; a dusty lo-fi sound |
## IAMAPARTY
- stems: 10 (10 usable)
- grid: **105.5 bpm** from `all drums` · confidence 46% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `kick` claimed *hits*, measured **bass**
- `synth` claimed *tonal*, measured **drums**
- shipped 17 samples · tiers S×5 A×3 B×9
### `fred_iamaparty_bass` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_2b` | B | 0.733 | 2 | 105 | 4.55s | `bass` | an acid bassline; a punchy tight sound |
### `fred_iamaparty_drums` — 4
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_alldrums_8b` | S | 0.948 | 8 | 105 | 18.20s | `all drums` | a drum break; a smooth clean sound |
| 1 | `01_alldrums_1b` | S | 0.947 | 1 | 105 | 2.28s | `all drums` | a drum break; a warm sound |
| 2 | `02_alldrums_4b` | S | 0.910 | 4 | 105 | 9.10s | `all drums` | a drum break; a warm sound |
| 3 | `03_hihatsetc_1b` | B | 0.707 | 1 | 105 | 2.28s | `hihats etc` | a drum break; an airy breathy sound |
### `fred_iamaparty_fx` — 4
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_fx_chop` | S | 0.965 | — | 105 | 0.56s | `fx` | a drum break; a harsh aggressive sound |
| 1 | `01_drones_1b` | A | 0.804 | 1 | 105 | 2.28s | `drones` | an electric guitar; a lush rich sound |
| 2 | `02_drones_2b` | B | 0.793 | 2 | 105 | 4.55s | `drones` | an electric guitar; a metallic sound |
| 3 | `03_drones_chop` | B | 0.784 | — | 105 | 0.30s | `drones` | an electric guitar; a metallic sound |
### `fred_iamaparty_hits` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_snare_chop` | A | 0.870 | — | 105 | 0.70s | `snare` | a snare drum; a punchy tight sound |
| 1 | `01_kick_chop` | B | 0.742 | — | 105 | 1.16s | `kick` | a kick drum; a punchy tight sound |
### `fred_iamaparty_tonal` — 3
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_synth_1b` | B | 0.739 | 1 | 105 | 2.28s | `synth` | a synth lead; a lush rich sound |
| 1 | `01_synth_4b` | B | 0.737 | 4 | 105 | 9.10s | `synth` | a synth lead; a lush rich sound |
| 2 | `02_synth_4b` | B | 0.719 | 4 | 105 | 9.10s | `synth` | a synth lead; a lush rich sound |
### `fred_iamaparty_vox` — 3
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_lv_chop` | S | 0.994 | — | 105 | 0.44s | `lv` | a spoken voice; a shadowy ténébreux sound |
| 1 | `01_bv_chop` | A | 0.804 | — | 105 | 0.30s | `bv` | an electric guitar; a woody organic sound |
| 2 | `02_lv_1b` | B | 0.728 | 1 | 105 | 2.28s | `lv` | a spoken voice; a dreamy washed-out sound |
## MAREA
- stems: 13 (12 usable, skipped near-silent: fx)
- grid: **123.0 bpm** from `perc` · confidence 57% · producer declared **123** (0.0% off)
- label vs measurement disagreements (measurement wins):
- `fx` claimed *fx*, measured **drums**
- `keys2` claimed *tonal*, measured **drums**
- `kick` claimed *hits*, measured **bass**
- `marea` claimed *tonal*, measured **drums**
- shipped 28 samples · tiers S×18 A×8 B×2
### `fred_marea_bass` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_2b` | S | 0.996 | 2 | 123 | 3.90s | `bass` | a sub bass; a punchy tight sound |
### `fred_marea_drums` — 12
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_kit_1b` | S | 0.988 | 1 | 123 | 1.95s | `kit` | a drum break; a punchy tight sound |
| 1 | `01_kit_1b` | S | 0.989 | 1 | 123 | 1.95s | `kit` | a drum break; a punchy tight sound |
| 2 | `02_kit_1b` | S | 0.987 | 1 | 123 | 1.95s | `kit` | a drum break; a punchy tight sound |
| 3 | `03_perc_2b` | S | 0.965 | 2 | 123 | 3.90s | `perc` | a drum break; a muddy boomy sound |
| 4 | `04_marea_1b` | S | 0.962 | 1 | 123 | 1.95s | `marea` | a spoken voice; an airy breathy sound |
| 5 | `05_alldrums_4b` | S | 0.940 | 4 | 123 | 7.80s | `all drums` | a drum break; a dusty lo-fi sound |
| 6 | `06_alldrums_1b` | S | 1.000 | 1 | 123 | 1.95s | `all drums` | a drum break; a warm sound |
| 7 | `07_alldrums_1b` | S | 0.999 | 1 | 123 | 1.95s | `all drums` | a drum break; a dark sound |
| 8 | `08_perc_8b` | A | 0.896 | 8 | 123 | 15.60s | `perc` | a hi-hat; a dusty lo-fi sound |
| 9 | `09_perc_8b` | A | 0.896 | 8 | 123 | 15.60s | `perc` | a hi-hat; a dusty lo-fi sound |
| 10 | `10_marea_4b` | A | 0.874 | 4 | 123 | 7.80s | `marea` | a spoken voice; a dreamy washed-out sound |
| 11 | `11_marea_4b` | A | 0.829 | 4 | 123 | 7.80s | `marea` | a synth pad; a dreamy washed-out sound |
### `fred_marea_hits` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_kick_chop` | S | 0.996 | — | 123 | 0.49s | `kick` | a kick drum; a punchy tight sound |
| 1 | `01_clap_chop` | A | 0.866 | — | 123 | 0.50s | `clap` | hand percussion; a smooth clean sound |
### `fred_marea_tonal` — 5
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_keys1_2b` | S | 0.968 | 2 | 123 | 3.90s | `keys1` | a synth pad; a dreamy washed-out sound |
| 1 | `01_keys2_1b` | S | 0.977 | 1 | 123 | 1.95s | `keys2` | a synth lead; a dreamy washed-out sound |
| 2 | `02_keys2_2b` | S | 0.970 | 2 | 123 | 3.90s | `keys2` | a synth lead; a dreamy washed-out sound |
| 3 | `03_keys2_2b` | S | 0.969 | 2 | 123 | 3.90s | `keys2` | a solo singing voice; a dreamy washed-out sound |
| 4 | `04_keys1_2b` | A | 0.887 | 2 | 123 | 3.90s | `keys1` | a hammond organ; a dreamy washed-out sound |
### `fred_marea_vox` — 8
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_tonivox_chop` | S | 0.999 | — | 123 | 0.53s | `toni vox` | a saxophone; a metallic sound |
| 1 | `01_makeitthruvox_chop` | S | 0.994 | — | 123 | 0.50s | `make it thru vox` | a drum break; a punchy tight sound |
| 2 | `02_breaths_1b` | S | 0.939 | 1 | 123 | 1.95s | `breaths` | a hi-hat; an airy breathy sound |
| 3 | `03_makeitthruvox_2b` | S | 0.940 | 2 | 123 | 3.90s | `make it thru vox` | a solo singing voice; a dreamy washed-out sound |
| 4 | `04_makeitthruvox_1b` | A | 0.896 | 1 | 123 | 1.95s | `make it thru vox` | a sitar or eastern string instrument; a dreamy washed-out sound |
| 5 | `05_tonivox_2b` | A | 0.858 | 2 | 123 | 3.90s | `toni vox` | a solo singing voice; an airy breathy sound |
| 6 | `06_breaths_1b` | B | 0.751 | 1 | 123 | 1.95s | `breaths` | a saxophone; an airy breathy sound |
| 7 | `07_breaths_1b` | B | 0.745 | 1 | 123 | 1.95s | `breaths` | a spoken voice; an airy breathy sound |
## ME
- stems: 6 (6 usable)
- grid: **152.0 bpm** from `morph` · confidence 96% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `morph` claimed *fx*, measured **drums**
- shipped 8 samples · tiers S×6 B×2
### `fred_me_bass` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_1b` | B | 0.757 | 1 | 152 | 1.58s | `bass` | a sub bass; a dark sound |
### `fred_me_fx` — 4
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_morph_chop` | S | 0.999 | — | 152 | 0.63s | `morph` | a saxophone; a punchy tight sound |
| 1 | `01_morph_4b` | S | 0.994 | 4 | 152 | 6.32s | `morph` | an acid bassline; a lush rich sound |
| 2 | `02_morph_2b` | S | 0.991 | 2 | 152 | 3.16s | `morph` | a saxophone; a lush rich sound |
| 3 | `03_textures_chop` | S | 0.944 | — | 152 | 0.48s | `textures` | hand percussion; a harsh aggressive sound |
### `fred_me_tonal` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_keys_1b` | B | 0.775 | 1 | 152 | 1.58s | `keys` | a grand piano; a dreamy washed-out sound |
### `fred_me_vox` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_lv_chop` | S | 0.996 | — | 152 | 0.64s | `lv` | a solo singing voice; a dusty lo-fi sound |
| 1 | `01_voxtextures_chop` | S | 0.995 | — | 152 | 0.36s | `vox textures` | a solo singing voice; an airy breathy sound |
## PLEASEMAKEITBETTER
- stems: 9 (9 usable)
- grid: **123.0 bpm** from `strings` · confidence 20% · **no usable pulse — chops only** · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `kick` claimed *hits*, measured **bass**
- `strings` claimed *tonal*, measured **drums**
- `toms` claimed *hits*, measured **bass**
- shipped 4 samples · tiers S×4
### `fred_pleasemakeitbetter_hits` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_toms_chop` | S | 1.000 | — | 123 | 0.35s | `toms` | a kick drum; a punchy tight sound |
| 1 | `01_kick_chop` | S | 0.999 | — | 123 | 0.44s | `kick` | a kick drum; a punchy tight sound |
### `fred_pleasemakeitbetter_vox` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_lv_chop` | S | 0.997 | — | 123 | 0.42s | `lv` | hand percussion; a shadowy ténébreux sound |
| 1 | `01_bv_chop` | S | 0.989 | — | 123 | 0.46s | `bv` | hand percussion; an airy breathy sound |
## YASMINAH
- stems: 10 (10 usable)
- grid: **117.5 bpm** from `all drums` · confidence 91% · no declared tempo to check against
- label vs measurement disagreements (measurement wins):
- `bass` claimed *bass*, measured **drums**
- `kick` claimed *hits*, measured **bass**
- shipped 20 samples · tiers S×11 A×5 B×4
### `fred_yasminah_bass` — 2
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_bass_4b` | B | 0.738 | 4 | 117 | 8.17s | `bass` | an acid bassline; a fat thick sound |
| 1 | `01_bass_2b` | B | 0.738 | 2 | 117 | 4.09s | `bass` | an acid bassline; a fat thick sound |
### `fred_yasminah_drums` — 10
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_drumloop1_1b` | S | 1.000 | 1 | 117 | 2.04s | `drum loop 1` | a drum break; a dusty lo-fi sound |
| 1 | `01_drumloop1_2b` | S | 1.000 | 2 | 117 | 4.09s | `drum loop 1` | a drum break; a dusty lo-fi sound |
| 2 | `02_drumloop1_1b` | S | 1.000 | 1 | 117 | 2.04s | `drum loop 1` | a drum break; a dusty lo-fi sound |
| 3 | `03_alldrums_1b` | S | 0.999 | 1 | 117 | 2.04s | `all drums` | a drum break; a smooth clean sound |
| 4 | `04_alldrums_1b` | S | 0.993 | 1 | 117 | 2.04s | `all drums` | a drum break; a smooth clean sound |
| 5 | `05_lodrums_4b` | S | 0.949 | 4 | 117 | 8.17s | `lo drums` | a drum break; a woody organic sound |
| 6 | `06_hidrums_1b` | S | 0.911 | 1 | 117 | 2.04s | `hi drums` | a kick drum; a punchy tight sound |
| 7 | `07_hidrums_1b` | S | 0.909 | 1 | 117 | 2.04s | `hi drums` | a hi-hat; a punchy tight sound |
| 8 | `08_lodrums_8b` | S | 0.902 | 8 | 117 | 16.35s | `lo drums` | a snare drum; a woody organic sound |
| 9 | `09_lodrums_2b` | A | 0.893 | 2 | 117 | 4.09s | `lo drums` | a saxophone; a woody organic sound |
### `fred_yasminah_hits` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_kick_chop` | A | 0.900 | — | 117 | 0.51s | `kick` | a kick drum; a punchy tight sound |
### `fred_yasminah_tonal` — 1
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_keys_4b` | B | 0.711 | 4 | 117 | 8.17s | `keys` | a synth pad; a dreamy washed-out sound |
### `fred_yasminah_vox` — 6
| n | file | tier | grade | bars | bpm | dur | from | CLAP |
|--:|---|---|--:|--:|--:|--:|---|---|
| 0 | `00_lv_chop` | S | 0.971 | — | 117 | 0.55s | `lv` | a spoken voice; an airy breathy sound |
| 1 | `01_bv_2b` | S | 0.949 | 2 | 117 | 4.09s | `bv` | a solo singing voice; a dreamy washed-out sound |
| 2 | `02_bv_chop` | A | 0.871 | — | 117 | 0.38s | `bv` | a drum break; a dark sound |
| 3 | `03_bv_1b` | A | 0.845 | 1 | 117 | 2.04s | `bv` | a flute; a dreamy washed-out sound |
| 4 | `04_lv_1b` | A | 0.802 | 1 | 117 | 2.04s | `lv` | a spoken voice; an airy breathy sound |
| 5 | `05_lv_2b` | B | 0.708 | 2 | 117 | 4.09s | `lv` | a solo singing voice; a dusty lo-fi sound |
## Worth a second listen
CLAP's instrument tag contradicts the kit these landed in. Nothing is re-filed on this basis — CLAP is the one signal here that can separate a sung line from a keyboard, so it is used to say what to audition first, not to decide.
| kit | file | from stem | filed as | CLAP hears | conf |
|---|---|---|---|---|--:|
| `fred_angie_fx` | `01_fx_1b` | `fx` | fx | drums (hand percussion) | 0.28 |
| `fred_angieinterlude_bass` | `00_pad_2b` | `pad` | bass | tonal (a synth pad) | 0.60 |
| `fred_angieinterlude_fx` | `02_noise_4b` | `noise` | fx | bass (a sub bass) | 0.33 |
| `fred_angieinterlude_fx` | `03_noise_1b` | `noise` | fx | drums (hand percussion) | 0.22 |
| `fred_bighen_bass` | `01_bass_8b` | `bass` | bass | drums (a kick drum) | 0.53 |
| `fred_bighen_fx` | `00_fx_1b` | `fx` | fx | drums (hand percussion) | 0.89 |
| `fred_dermot_tonal` | `01_guitar_1b` | `guitar` | tonal | drums (hand percussion) | 0.19 |
| `fred_dermot_tonal` | `02_guitar_4b` | `guitar` | tonal | bass (an acid bassline) | 0.18 |
| `fred_iamaparty_fx` | `00_fx_chop` | `fx` | fx | drums (a drum break) | 0.59 |
| `fred_marea_drums` | `04_marea_1b` | `marea` | drums | vox (a spoken voice) | 0.53 |
| `fred_marea_drums` | `10_marea_4b` | `marea` | drums | vox (a spoken voice) | 0.69 |
| `fred_marea_drums` | `11_marea_4b` | `marea` | drums | tonal (a synth pad) | 0.22 |
| `fred_marea_vox` | `01_makeitthruvox_chop` | `make it thru vox` | vox | drums (a drum break) | 0.28 |
| `fred_marea_vox` | `02_breaths_1b` | `breaths` | vox | drums (a hi-hat) | 0.46 |
| `fred_me_fx` | `01_morph_4b` | `morph` | fx | bass (an acid bassline) | 0.32 |
| `fred_me_fx` | `03_textures_chop` | `textures` | fx | drums (hand percussion) | 0.28 |
| `fred_pleasemakeitbetter_vox` | `00_lv_chop` | `lv` | vox | drums (hand percussion) | 0.29 |
| `fred_pleasemakeitbetter_vox` | `01_bv_chop` | `bv` | vox | drums (hand percussion) | 0.33 |
| `fred_yasminah_drums` | `09_lodrums_2b` | `lo drums` | drums | tonal (a saxophone) | 0.38 |
| `fred_yasminah_vox` | `02_bv_chop` | `bv` | vox | drums (a drum break) | 0.33 |
-- fred kits — generated by engine.stempack; n indices are sort order
-- fred_angie_bass (3 samples, 132.51 bpm)
-- n 0 00_bass_8b S loopAt 8 bass · an acid bassline
-- n 1 01_bass_8b S loopAt 8 bass · an acid bassline
-- n 2 02_bass_2b S loopAt 2 bass · an acid bassline
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 8 $ chop 8 $ s "fred_angie_bass" # n 0
-- fred_angie_drums (3 samples, 132.51 bpm)
-- n 0 00_drumbreaks_2b S loopAt 2 drum breaks · a drum break
-- n 1 01_alldrums_1b S loopAt 1 all drums · a drum break
-- n 2 02_alldrums_2b A loopAt 2 all drums · a drum break
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_angie_drums" # n 0
-- fred_angie_fx (2 samples, 132.51 bpm)
-- n 0 00_fx_chop S one-shot 0.5 fx · a spoken voice
-- n 1 01_fx_1b A loopAt 1 fx · hand percussion
-- d1 $ s "fred_angie_fx*4" # n (irand 2)
-- fred_angie_hits (2 samples, 132.51 bpm)
-- n 0 00_kicks_chop S one-shot 0.4 kicks · a kick drum
-- n 1 01_snare_chop S one-shot 1.8 snare · a hi-hat
-- d1 $ s "fred_angie_hits*4" # n (irand 2)
-- fred_angie_tonal (5 samples, 132.51 bpm)
-- n 0 00_synth_8b S loopAt 8 synth · a synth pad
-- n 1 01_synth_2b B loopAt 2 synth · a synth pad
-- n 2 02_synth_2b B loopAt 2 synth · an electric guitar
-- n 3 03_padstuff_2b B loopAt 2 pad stuff · an electric guitar
-- n 4 04_padstuff_1b B loopAt 1 pad stuff · a synth lead
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 8 $ chop 8 $ s "fred_angie_tonal" # n 0
-- fred_angie_vox (7 samples, 132.51 bpm)
-- n 0 00_bv_chop S one-shot 0.6 bv · a spoken voice
-- n 1 01_lv_chop S one-shot 0.2 lv · a saxophone
-- n 2 02_lv_4b B loopAt 4 lv · a solo singing voice
-- n 3 03_lv_8b B loopAt 8 lv · a solo singing voice
-- n 4 04_lv_2b B loopAt 2 lv · a solo singing voice
-- n 5 05_bv_1b B loopAt 1 bv · a spoken voice
-- n 6 06_bv_1b B loopAt 1 bv · a solo singing voice
-- d1 $ s "fred_angie_vox*4" # n (irand 7)
-- fred_angieinterlude_bass (1 samples, 112.35 bpm)
-- n 0 00_pad_2b S loopAt 2 pad · a synth pad
-- setcps (0.4681) -- 112.35 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_angieinterlude_bass" # n 0
-- fred_angieinterlude_fx (4 samples, 112.35 bpm)
-- n 0 00_ipadtextures_chop S one-shot 0.3 ipad textures · a saxophone
-- n 1 01_drone_chop S one-shot 0.3 drone · a rhodes electric piano
-- n 2 02_noise_4b A loopAt 4 noise · a sub bass
-- n 3 03_noise_1b A loopAt 1 noise · hand percussion
-- d1 $ s "fred_angieinterlude_fx*4" # n (irand 4)
-- fred_angieinterlude_hits (1 samples, 112.35 bpm)
-- n 0 00_kick_chop S one-shot 0.5 kick · a kick drum
-- d1 $ s "fred_angieinterlude_hits*4" # n (irand 1)
-- fred_angieinterlude_vox (2 samples, 112.35 bpm)
-- n 0 00_voices_chop S one-shot 0.4 voices · a spoken voice
-- n 1 01_fvox_chop S one-shot 0.3 f vox · a spoken voice
-- d1 $ s "fred_angieinterlude_vox*4" # n (irand 2)
-- fred_bighen_bass (3 samples, 132.51 bpm)
-- n 0 00_bass_8b S loopAt 8 bass · a sub bass
-- n 1 01_bass_8b S loopAt 8 bass · a kick drum
-- n 2 02_bass_4b B loopAt 4 bass · a sub bass
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 8 $ chop 8 $ s "fred_bighen_bass" # n 0
-- fred_bighen_drums (9 samples, 132.51 bpm)
-- n 0 00_maindrumloop_2b S loopAt 2 main drum loop · a drum break
-- n 1 01_alldrums_2b S loopAt 2 all drums · a drum break
-- n 2 02_drumbreaks_1b S loopAt 1 drum breaks · a drum break
-- n 3 03_maindrumloop_1b B loopAt 1 main drum loop · a drum break
-- n 4 04_maindrumloop_8b B loopAt 8 main drum loop · a drum break
-- n 5 05_alldrums_4b B loopAt 4 all drums · a drum break
-- n 6 06_alldrums_8b B loopAt 8 all drums · a drum break
-- n 7 07_drumbreaks_4b B loopAt 4 drum breaks · a drum break
-- n 8 08_drumbreaks_8b B loopAt 8 drum breaks · a drum break
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_bighen_drums" # n 0
-- fred_bighen_fx (1 samples, 132.51 bpm)
-- n 0 00_fx_1b S loopAt 1 fx · hand percussion
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_bighen_fx" # n 0
-- fred_bighen_hits (1 samples, 132.51 bpm)
-- n 0 00_snare_chop A one-shot 0.3 snare · a hi-hat
-- d1 $ s "fred_bighen_hits*4" # n (irand 1)
-- fred_bighen_tonal (2 samples, 132.51 bpm)
-- n 0 00_strings_2b S loopAt 2 strings · an electric guitar
-- n 1 01_synth_2b S loopAt 2 synth · a rhodes electric piano
-- setcps (0.5521) -- 132.51 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_bighen_tonal" # n 0
-- fred_bighen_vox (8 samples, 132.51 bpm)
-- n 0 00_lv_chop S one-shot 0.3 lv · a solo singing voice
-- n 1 01_lv_4b S loopAt 4 lv · a solo singing voice
-- n 2 02_lv_1b S loopAt 1 lv · a solo singing voice
-- n 3 03_lv_2b S loopAt 2 lv · a synth lead
-- n 4 04_bv_2b S loopAt 2 bv · an electric guitar
-- n 5 05_bv_chop S one-shot 1.7 bv · an electric guitar
-- n 6 06_bv_1b A loopAt 1 bv · a spoken voice
-- n 7 07_bv_4b A loopAt 4 bv · a synth lead
-- d1 $ s "fred_bighen_vox*4" # n (irand 8)
-- fred_dermot_bass (2 samples, 152 bpm)
-- n 0 00_bass_8b S loopAt 8 bass · an acid bassline
-- n 1 01_bass_2b S loopAt 2 bass · a sub bass
-- setcps (0.6333) -- 152 bpm
-- d1 $ loopAt 8 $ chop 8 $ s "fred_dermot_bass" # n 0
-- fred_dermot_drums (7 samples, 152 bpm)
-- n 0 00_drumbreaks_1b S loopAt 1 drum breaks · a drum break
-- n 1 01_drumbreaks_8b S loopAt 8 drum breaks · a drum break
-- n 2 02_alldrums_1b S loopAt 1 all drums · a drum break
-- n 3 03_alldrums_4b S loopAt 4 all drums · a drum break
-- n 4 04_alldrums_2b A loopAt 2 all drums · a drum break
-- n 5 05_hihatsetc_4b A loopAt 4 hihats etc · a hi-hat
-- n 6 06_hihatsetc_1b A loopAt 1 hihats etc · hand percussion
-- setcps (0.6333) -- 152 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_dermot_drums" # n 0
-- fred_dermot_hits (2 samples, 152 bpm)
-- n 0 00_snare_chop S one-shot 0.5 snare · a drum break
-- n 1 01_kick_chop S one-shot 0.4 kick · a kick drum
-- d1 $ s "fred_dermot_hits*4" # n (irand 2)
-- fred_dermot_tonal (3 samples, 152 bpm)
-- n 0 00_keys1_2b A loopAt 2 keys1 · a synth pad
-- n 1 01_guitar_1b B loopAt 1 guitar · hand percussion
-- n 2 02_guitar_4b B loopAt 4 guitar · an acid bassline
-- setcps (0.6333) -- 152 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_dermot_tonal" # n 0
-- fred_dermot_vox (5 samples, 152 bpm)
-- n 0 00_lv_chop S one-shot 0.5 lv · an electric guitar
-- n 1 01_choir_chop A one-shot 0.4 choir · an electric guitar
-- n 2 02_bv_chop B one-shot 1.8 bv · a saxophone
-- n 3 03_lv_8b B loopAt 8 lv · a solo singing voice
-- n 4 04_lv_2b B loopAt 2 lv · a solo singing voice
-- d1 $ s "fred_dermot_vox*4" # n (irand 5)
-- fred_iamaparty_bass (1 samples, 105.47 bpm)
-- n 0 00_bass_2b B loopAt 2 bass · an acid bassline
-- setcps (0.4395) -- 105.47 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_iamaparty_bass" # n 0
-- fred_iamaparty_drums (4 samples, 105.47 bpm)
-- n 0 00_alldrums_8b S loopAt 8 all drums · a drum break
-- n 1 01_alldrums_1b S loopAt 1 all drums · a drum break
-- n 2 02_alldrums_4b S loopAt 4 all drums · a drum break
-- n 3 03_hihatsetc_1b B loopAt 1 hihats etc · a drum break
-- setcps (0.4395) -- 105.47 bpm
-- d1 $ loopAt 8 $ chop 8 $ s "fred_iamaparty_drums" # n 0
-- fred_iamaparty_fx (4 samples, 105.47 bpm)
-- n 0 00_fx_chop S one-shot 0.5 fx · a drum break
-- n 1 01_drones_1b A loopAt 1 drones · an electric guitar
-- n 2 02_drones_2b B loopAt 2 drones · an electric guitar
-- n 3 03_drones_chop B one-shot 0.3 drones · an electric guitar
-- d1 $ s "fred_iamaparty_fx*4" # n (irand 4)
-- fred_iamaparty_hits (2 samples, 105.47 bpm)
-- n 0 00_snare_chop A one-shot 0.7 snare · a snare drum
-- n 1 01_kick_chop B one-shot 1.1 kick · a kick drum
-- d1 $ s "fred_iamaparty_hits*4" # n (irand 2)
-- fred_iamaparty_tonal (3 samples, 105.47 bpm)
-- n 0 00_synth_1b B loopAt 1 synth · a synth lead
-- n 1 01_synth_4b B loopAt 4 synth · a synth lead
-- n 2 02_synth_4b B loopAt 4 synth · a synth lead
-- setcps (0.4395) -- 105.47 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_iamaparty_tonal" # n 0
-- fred_iamaparty_vox (3 samples, 105.47 bpm)
-- n 0 00_lv_chop S one-shot 0.4 lv · a spoken voice
-- n 1 01_bv_chop A one-shot 0.3 bv · an electric guitar
-- n 2 02_lv_1b B loopAt 1 lv · a spoken voice
-- d1 $ s "fred_iamaparty_vox*4" # n (irand 3)
-- fred_marea_bass (1 samples, 123.05 bpm)
-- n 0 00_bass_2b S loopAt 2 bass · a sub bass
-- setcps (0.5127) -- 123.05 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_marea_bass" # n 0
-- fred_marea_drums (12 samples, 123.05 bpm)
-- n 0 00_kit_1b S loopAt 1 kit · a drum break
-- n 1 01_kit_1b S loopAt 1 kit · a drum break
-- n 2 02_kit_1b S loopAt 1 kit · a drum break
-- n 3 03_perc_2b S loopAt 2 perc · a drum break
-- n 4 04_marea_1b S loopAt 1 marea · a spoken voice
-- n 5 05_alldrums_4b S loopAt 4 all drums · a drum break
-- n 6 06_alldrums_1b S loopAt 1 all drums · a drum break
-- n 7 07_alldrums_1b S loopAt 1 all drums · a drum break
-- n 8 08_perc_8b A loopAt 8 perc · a hi-hat
-- n 9 09_perc_8b A loopAt 8 perc · a hi-hat
-- n 10 10_marea_4b A loopAt 4 marea · a spoken voice
-- n 11 11_marea_4b A loopAt 4 marea · a synth pad
-- setcps (0.5127) -- 123.05 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_marea_drums" # n 0
-- fred_marea_hits (2 samples, 123.05 bpm)
-- n 0 00_kick_chop S one-shot 0.4 kick · a kick drum
-- n 1 01_clap_chop A one-shot 0.5 clap · hand percussion
-- d1 $ s "fred_marea_hits*4" # n (irand 2)
-- fred_marea_tonal (5 samples, 123.05 bpm)
-- n 0 00_keys1_2b S loopAt 2 keys1 · a synth pad
-- n 1 01_keys2_1b S loopAt 1 keys2 · a synth lead
-- n 2 02_keys2_2b S loopAt 2 keys2 · a synth lead
-- n 3 03_keys2_2b S loopAt 2 keys2 · a solo singing voice
-- n 4 04_keys1_2b A loopAt 2 keys1 · a hammond organ
-- setcps (0.5127) -- 123.05 bpm
-- d1 $ loopAt 2 $ chop 8 $ s "fred_marea_tonal" # n 0
-- fred_marea_vox (8 samples, 123.05 bpm)
-- n 0 00_tonivox_chop S one-shot 0.5 toni vox · a saxophone
-- n 1 01_makeitthruvox_chop S one-shot 0.5 make it thru vox · a drum break
-- n 2 02_breaths_1b S loopAt 1 breaths · a hi-hat
-- n 3 03_makeitthruvox_2b S loopAt 2 make it thru vox · a solo singing voice
-- n 4 04_makeitthruvox_1b A loopAt 1 make it thru vox · a sitar or eastern string instrument
-- n 5 05_tonivox_2b A loopAt 2 toni vox · a solo singing voice
-- n 6 06_breaths_1b B loopAt 1 breaths · a saxophone
-- n 7 07_breaths_1b B loopAt 1 breaths · a spoken voice
-- d1 $ s "fred_marea_vox*4" # n (irand 8)
-- fred_me_bass (1 samples, 152 bpm)
-- n 0 00_bass_1b B loopAt 1 bass · a sub bass
-- setcps (0.6333) -- 152 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_me_bass" # n 0
-- fred_me_fx (4 samples, 152 bpm)
-- n 0 00_morph_chop S one-shot 0.6 morph · a saxophone
-- n 1 01_morph_4b S loopAt 4 morph · an acid bassline
-- n 2 02_morph_2b S loopAt 2 morph · a saxophone
-- n 3 03_textures_chop S one-shot 0.4 textures · hand percussion
-- d1 $ s "fred_me_fx*4" # n (irand 4)
-- fred_me_tonal (1 samples, 152 bpm)
-- n 0 00_keys_1b B loopAt 1 keys · a grand piano
-- setcps (0.6333) -- 152 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_me_tonal" # n 0
-- fred_me_vox (2 samples, 152 bpm)
-- n 0 00_lv_chop S one-shot 0.6 lv · a solo singing voice
-- n 1 01_voxtextures_chop S one-shot 0.3 vox textures · a solo singing voice
-- d1 $ s "fred_me_vox*4" # n (irand 2)
-- fred_pleasemakeitbetter_hits (2 samples, 123.05 bpm)
-- n 0 00_toms_chop S one-shot 0.3 toms · a kick drum
-- n 1 01_kick_chop S one-shot 0.4 kick · a kick drum
-- d1 $ s "fred_pleasemakeitbetter_hits*4" # n (irand 2)
-- fred_pleasemakeitbetter_vox (2 samples, 123.05 bpm)
-- n 0 00_lv_chop S one-shot 0.4 lv · hand percussion
-- n 1 01_bv_chop S one-shot 0.4 bv · hand percussion
-- d1 $ s "fred_pleasemakeitbetter_vox*4" # n (irand 2)
-- fred_yasminah_bass (2 samples, 117.45 bpm)
-- n 0 00_bass_4b B loopAt 4 bass · an acid bassline
-- n 1 01_bass_2b B loopAt 2 bass · an acid bassline
-- setcps (0.4894) -- 117.45 bpm
-- d1 $ loopAt 4 $ chop 8 $ s "fred_yasminah_bass" # n 0
-- fred_yasminah_drums (10 samples, 117.45 bpm)
-- n 0 00_drumloop1_1b S loopAt 1 drum loop 1 · a drum break
-- n 1 01_drumloop1_2b S loopAt 2 drum loop 1 · a drum break
-- n 2 02_drumloop1_1b S loopAt 1 drum loop 1 · a drum break
-- n 3 03_alldrums_1b S loopAt 1 all drums · a drum break
-- n 4 04_alldrums_1b S loopAt 1 all drums · a drum break
-- n 5 05_lodrums_4b S loopAt 4 lo drums · a drum break
-- n 6 06_hidrums_1b S loopAt 1 hi drums · a kick drum
-- n 7 07_hidrums_1b S loopAt 1 hi drums · a hi-hat
-- n 8 08_lodrums_8b S loopAt 8 lo drums · a snare drum
-- n 9 09_lodrums_2b A loopAt 2 lo drums · a saxophone
-- setcps (0.4894) -- 117.45 bpm
-- d1 $ loopAt 1 $ chop 8 $ s "fred_yasminah_drums" # n 0
-- fred_yasminah_hits (1 samples, 117.45 bpm)
-- n 0 00_kick_chop A one-shot 0.5 kick · a kick drum
-- d1 $ s "fred_yasminah_hits*4" # n (irand 1)
-- fred_yasminah_tonal (1 samples, 117.45 bpm)
-- n 0 00_keys_4b B loopAt 4 keys · a synth pad
-- setcps (0.4894) -- 117.45 bpm
-- d1 $ loopAt 4 $ chop 8 $ s "fred_yasminah_tonal" # n 0
-- fred_yasminah_vox (6 samples, 117.45 bpm)
-- n 0 00_lv_chop S one-shot 0.5 lv · a spoken voice
-- n 1 01_bv_2b S loopAt 2 bv · a solo singing voice
-- n 2 02_bv_chop A one-shot 0.3 bv · a drum break
-- n 3 03_bv_1b A loopAt 1 bv · a flute
-- n 4 04_lv_1b A loopAt 1 lv · a spoken voice
-- n 5 05_lv_2b B loopAt 2 lv · a solo singing voice
-- d1 $ s "fred_yasminah_vox*4" # n (irand 6)
# kit check — 150 samples, 144 clean, 6 flagged
## `fred_angie_tonal`
-`03_padstuff_2b` — weak bar periodicity (0.00 < 0.05 for tonal) — bar-aligned but not a repeating unit
## `fred_angieinterlude_bass`
-`00_pad_2b` — weak bar periodicity (0.03 < 0.05 for bass) — bar-aligned but not a repeating unit
## `fred_bighen_tonal`
-`01_synth_2b` — weak bar periodicity (0.02 < 0.05 for tonal) — bar-aligned but not a repeating unit
## `fred_marea_drums`
-`10_marea_4b` — weak bar periodicity (0.12 < 0.30 for drums) — bar-aligned but not a repeating unit
-`11_marea_4b` — weak bar periodicity (0.08 < 0.30 for drums) — bar-aligned but not a repeating unit
## `fred_yasminah_tonal`
-`00_keys_4b` — weak bar periodicity (0.02 < 0.05 for tonal) — bar-aligned but not a repeating unit
**Distinct sounds:** 150 of 150 (0 near-duplicates)
\ No newline at end of file
#!/usr/bin/env python3
"""restage — apply kit-wide gain staging to sample kits that are already on disk.
`engine.stempack` stages a kit's level as it exports, but kits cut before that (or by
hand, in Audacity) carry whatever level their source had. Producer stems come off a loud
commercial master and land at full scale, which SuperDirt cannot use: amp = gain^4, so
`# gain 1.2` is +7.6 dB and the orbit clips the moment you touch it.
This applies the SAME function `stempack` uses at export (`kit_headroom_gain` — one gain
per kit, attenuate only), so a kit restaged here is bit-identical to one cut with the
fix in place. Reads and rewrites in place, 24-bit, and reports what it changed.
Idempotent: a kit already within `DEAD_BAND_DB` of the ceiling is left untouched,
so re-running never requantizes audio for a gain change nobody can hear.
python3 restage.py <kit dir> [<kit dir> …] # explicit kits
python3 restage.py --cuts fred_kits.json # every kit named in a cut manifest
python3 restage.py --all-under ~/Work/Sound/Samples --dry-run
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import numpy as np
import soundfile as sf
sys.path.insert(0, str(Path(__file__).resolve().parent))
from engine.stempack import HEADROOM_CEILING_DB, kit_headroom_gain # noqa: E402
# An attenuation smaller than this is not applied. Export stages in float and writes
# PCM_24, so the peak that comes back off disk differs from the one it aimed at by up to
# a quantization step — enough that a freshly-staged kit measures a hair ABOVE the
# ceiling and asks to be staged again, by -0.00003 dB, forever. Each of those rounds is a
# real read-modify-write and a fresh 24-bit requantization of every file in the kit, so
# the non-idempotence is not merely untidy. 0.05 dB is inaudible and four orders of
# magnitude above the quantization floor.
DEAD_BAND_DB = 0.05
def _peak_db(path: Path) -> float:
y, _ = sf.read(str(path), always_2d=True, dtype="float32")
return float(20 * np.log10(np.max(np.abs(y)) + 1e-12))
def restage_kit(kit_dir: Path, *, ceiling_db: float = HEADROOM_CEILING_DB,
dry_run: bool = False) -> dict:
"""Bring one kit's hottest file to the ceiling, moving every file by the same gain."""
wavs = sorted(p for p in kit_dir.glob("*.wav") if not p.name.startswith("."))
if not wavs:
return {"kit": kit_dir.name, "files": 0, "gain_db": 0.0, "changed": False}
peaks = [_peak_db(p) for p in wavs]
g = kit_headroom_gain(peaks, ceiling_db)
gain_db = 20 * np.log10(g) if g > 0 else 0.0
if g >= 1.0 or -gain_db < DEAD_BAND_DB:
return {"kit": kit_dir.name, "files": len(wavs), "gain_db": 0.0,
"changed": False, "peak_before": max(peaks)}
if not dry_run:
for p in wavs:
y, sr = sf.read(str(p), always_2d=True, dtype="float32")
sf.write(str(p), y * g, sr, subtype="PCM_24")
return {"kit": kit_dir.name, "files": len(wavs), "gain_db": round(float(gain_db), 2),
"changed": True, "peak_before": round(max(peaks), 2),
"peak_after": round(max(peaks) + float(gain_db), 2)}
def _regrade_manifest(cuts_path: Path, kits: set[str]) -> None:
"""Re-grade the restaged files and write the new grades back into the manifest."""
from engine import grade as G
cuts = json.loads(cuts_path.read_text())
n = 0
for c in cuts:
if c["kit"] not in kits or not Path(c["path"]).exists():
continue
g = G.grade(c["path"], role=c.get("family"))
c["grade"], c["tier"] = g.grade, g.tier
c["flags"] = list(g.flags)
n += 1
cuts_path.write_text(json.dumps(cuts, indent=2))
print(f" re-graded {n} entries in {cuts_path.name}")
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="restage", description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("kits", nargs="*", type=Path)
ap.add_argument("--cuts", type=Path, help="a stempack cuts JSON; restage every kit in it")
ap.add_argument("--all-under", type=Path, help="every immediate subdirectory of this path")
ap.add_argument("--ceiling", type=float, default=HEADROOM_CEILING_DB)
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args(argv)
dirs: list[Path] = list(a.kits)
if a.cuts:
cuts = json.loads(a.cuts.read_text())
dirs += sorted({Path(c["path"]).parent for c in cuts})
if a.all_under:
dirs += sorted(d for d in a.all_under.iterdir() if d.is_dir())
dirs = sorted({d.resolve() for d in dirs if d.is_dir()})
if not dirs:
ap.error("no kit directories given")
rows = [restage_kit(d, ceiling_db=a.ceiling, dry_run=a.dry_run) for d in dirs]
changed = [r for r in rows if r["changed"]]
# Whoever changes the audio owns the record of it. Restaging moves every grade that
# has a level term in it, so a manifest left alone would describe bytes that no longer
# exist — and `kitgate` compares the two precisely to catch that kind of drift.
if a.cuts and changed and not a.dry_run:
_regrade_manifest(a.cuts, {r["kit"] for r in changed})
for r in changed:
print(f" {r['kit']:34.34} {r['files']:3} files "
f"{r['peak_before']:+6.1f} → {r['peak_after']:+6.1f} dBFS "
f"({r['gain_db']:+.1f} dB)")
verb = "would restage" if a.dry_run else "restaged"
print(f"{verb} {len(changed)} of {len(rows)} kits "
f"(ceiling {a.ceiling:+.1f} dBFS; kits already below it are left alone)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
......@@ -64,9 +64,42 @@ def test_clipping_flagged():
assert "clipping" in G.grade_array(y, sr).flags
def test_near_silent_flagged():
def test_silence_is_empty_not_merely_near_silent():
"""-62 dBFS peak is not quiet material, it is nothing — and nothing used to grade A.
Every other sub-score is vacuously perfect on silence (a seam between two silences,
zero DC, trivial zero crossings), so the composite's optimum was "no audio at all"
and the finder walked straight into it: 24 of 153 loops cut from the Fred pack were
digital silence at -104…-75 dBFS, graded 0.707-0.896."""
y, sr = _sine(amp=0.0008) # ~ -62 dBFS
assert "near-silent" in G.grade_array(y, sr).flags
g = G.grade_array(y, sr)
assert "empty" in g.flags
assert g.grade == 0.0 and g.tier == "D"
def test_a_sparse_loop_is_near_silent_but_not_empty():
"""RMS and peak answer different questions. A hat loop with peaks at -14.7 dBFS reads
-48.8 RMS because RMS measures how much SILENCE a sparse signal contains — that is
advisory, not disqualifying, and it must not be confused with an empty file."""
sr, rng = 44100, np.random.RandomState(0)
y = np.zeros(sr * 4)
for i in range(4): # four very short ticks in four seconds
j = i * sr
y[j:j + 32] = 0.3 * rng.randn(32) * np.hanning(32)
g = G.grade_array(y, sr)
assert g.metrics["peak_dbfs"] - g.metrics["rms_dbfs"] > 30 # a real sparse crest
g = G.grade_array(y, sr)
assert "near-silent" in g.flags # RMS is way down …
assert "empty" not in g.flags # … but there is clearly audio here
assert g.grade > 0.0
def test_an_anti_phase_pair_is_mono_incompatible_not_empty():
"""The presence test has to look at the channels, not the mono sum: L/-R sums to
exact zero, and calling that "empty" would suppress the one flag worth raising."""
mono, sr = _sine(periods=200)
g = G.grade_array(np.stack([mono, -mono]), sr)
assert "empty" not in g.flags
assert "mono-incompatible" in g.flags
def test_mono_compatible_vs_anti_phase():
......
......@@ -357,3 +357,108 @@ def test_beat_grid_recovers_tempo_not_its_octave():
assert len(times) >= 8
med = float(np.median(bpm))
assert 100 <= med <= 140 # not the 240 octave
def test_rotation_checks_its_own_work_and_repeats():
"""One rotation pass is not enough on real music. The roll point is zero-crossing
snapped by up to ~10 ms against a 20 ms attack window, which is enough to flip the
winner between two near-tied slots — so the loop can come out starting on a weak
beat, the "timed ok but cut BCDA/DABC" failure this function exists to prevent.
Measured over 60 shipped bar-loops: slot 0 was the best slot in 29 and in the worse
half in 10 after one pass; 46 and 1 with the check-and-repeat.
"""
import numpy as np
from engine import loops as L
sr, bpm, bars = 44100, 120.0, 4
nb = bars * 4
n = int(round(bars * 4 * 60.0 / bpm * sr))
beat = n // nb
y = np.zeros(n)
for b in range(nb): # strongest kick deliberately on slot 4
i = b * beat
env = np.exp(-np.arange(beat) / (sr * 0.04))
y[i:i + beat] += (np.sin(2 * np.pi * 55 * np.arange(beat) / sr) * env
* (1.0 if b == 4 else 0.35))
clip = y.astype(np.float32)[:, None]
rot, slot, _ = L._rotate_to_downbeat(clip, sr, nb)
assert slot == 4 # found the real downbeat
assert L._downbeat_miss(rot, sr, nb) <= 0.02 # and it now sits at 0
# rotation is free: a bar-exact loop keeps its length and its wrap
assert rot.shape == clip.shape
# idempotent — a rotated loop is already on its downbeat
_, slot2, _ = L._rotate_to_downbeat(rot, sr, nb)
assert slot2 == 0
def test_the_downbeat_scorer_is_shared_so_a_check_cannot_disagree():
"""The rotation and its verification must use the same scorer; a lookalike with a
different window produces disagreements that are the measurement's fault."""
import numpy as np
from engine import loops as L
sr, nb = 44100, 8
rs = np.random.RandomState(0)
y = (rs.randn(sr * 2) * 0.1).astype(np.float32)
s = L._downbeat_slot_scores(y, sr, nb)
assert s.shape == (nb,)
assert 0.0 <= L._downbeat_miss(y, sr, nb) <= 1.0
def test_the_finder_will_not_spend_its_candidate_slots_on_silence():
"""A stem that plays for a few bars and then rests has a degenerate optimum: every
term in the composite — structural self-similarity, seam, zero crossings — is
vacuously perfect across the rest. Measured on BIGHEN's strings stem, all six top
windows sat inside a gap, so filtering at export left the kit with one loop where it
should have had four.
The grid has to come from ANOTHER stem for this to reproduce, which is also how
`stempack` calls it: a per-stem grid finds no beats in the silence, so no window is
ever generated there and the bug cannot appear. Share a rhythmic track's grid — the
whole point of doing so — and every stem gets windows across the full duration,
including the parts where it is not playing.
"""
import numpy as np
from engine import loops as L
sr = 22050
bpm, bars = 120.0, 4
bar_s = 4 * 60.0 / bpm
def ticks(n_bars, seed):
r = np.random.RandomState(seed)
out = np.zeros(int(round(n_bars * bar_s * sr)), dtype=np.float32)
step = int(round(bar_s / 4 * sr))
env = np.exp(-np.arange(step) / (sr * 0.03))
for i in range(0, len(out) - step, step):
out[i:i + step] += (r.randn(step) * env * 0.4).astype(np.float32)
return out
drums = ticks(32, 1) # plays throughout: the grid source
strings = np.concatenate([ticks(8, 2), # plays for 8 bars …
np.zeros(len(drums) - int(round(8 * bar_s * sr)),
dtype=np.float32)]) # … then rests
assert len(strings) == len(drums)
grid = L.beat_grid(drums, sr)
cands = L.analyze_stem(strings, sr, bars=(bars,), top_n=6, grid=grid)
assert cands, "the played section should still yield candidates"
played_s = 8 * bar_s
assert all(c.start_s < played_s for c in cands), \
[(c.start_s, c.score) for c in cands]
def test_the_beat_peak_table_is_exact_not_an_approximation():
"""The presence gate reads a per-beat peak table instead of scanning each candidate
window, which is only legitimate if it returns the same number. The inter-beat
segments partition the window exactly, so it must — and if that ever stops being
true the gate would start rejecting real material with no visible symptom."""
import numpy as np
from engine import loops as L
sr = 8000
rng = np.random.RandomState(3)
y = (rng.randn(sr * 6) * 0.2).astype(np.float32)
times = np.arange(0, 6.0, 0.5)
bp = L._beat_peaks(y, sr, times)
for i in range(len(times) - 4):
e = i + 4
naive = float(np.max(np.abs(y[int(times[i] * sr):int(times[e] * sr)])))
assert float(np.max(bp[i:e])) == naive, (i, e)
"""Producer stem-pack batching: name parsing, label verification, and the kit audit.
The DSP-heavy paths are exercised end-to-end on real stems; what is locked here is the
logic that decides *what a stem is* and *whether a shipped loop is defensible* — the two
places a silent wrong answer would propagate into a kit nobody notices is bad.
cd tools/foundry && python3 -m pytest tests/test_stempack.py -q
"""
import sys
from pathlib import Path
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from engine import kitcheck as K # noqa: E402
from engine import roles as R # noqa: E402
SR = 44100
# ── name parsing ─────────────────────────────────────────────────────────────
def test_parses_the_pack_scaffolding():
n = R.parse_stem_name("MAREA MIX10 123BPM KIT STEM.wav")
assert n.track == "MAREA"
assert n.role_token == "kit"
assert n.claimed_family == "drums"
assert n.bpm_hint == 123.0
def test_multiword_roles_and_the_NEW_variant():
assert R.parse_stem_name("ANGIE MIX18 PAD STUFF STEM.wav").role_token == "pad stuff"
assert R.parse_stem_name("ANGIE NEW F VOX STEM.wav").claimed_family == "vox"
assert R.parse_stem_name("BIG HEN MIX1 ALL DRUMS STEM.wav").claimed_family == "drums"
def test_isolated_hits_are_not_the_same_family_as_a_groove():
"""A KICK stem and an ALL DRUMS stem both measure percussive; only the name
separates a one-shot source from loop material, so the map must keep them apart."""
assert R.parse_stem_name("IAMAPARTY MIX1 KICK STEM.wav").claimed_family == "hits"
assert R.parse_stem_name("IAMAPARTY MIX1 ALL DRUMS STEM.wav").claimed_family == "drums"
def test_unknown_token_falls_back_on_a_contained_word():
"""`MAKE IT THRU VOX` is track-specific, but it still says VOX."""
n = R.parse_stem_name("MAREA MIX10 123BPM MAKE IT THRU VOX STEM.wav")
assert n.claimed_family == "vox"
def test_a_wholly_unknown_token_is_soft_not_fatal():
n = R.parse_stem_name("MAREA MIX10 123BPM MAREA STEM.wav")
assert n.known is False
assert n.claimed_family == "tonal" # a default the probe is free to override
# ── the probe overrides the label ────────────────────────────────────────────
def _sine(f, secs=6.0, amp=0.3):
t = np.arange(int(SR * secs)) / SR
return (amp * np.sin(2 * np.pi * f * t)).astype(np.float32)
def test_a_sub_sine_measures_as_bass_whatever_the_name_says():
p = R.probe(_sine(50), SR, claimed="tonal")
assert p.measured_family == "bass"
assert not p.agrees
assert "measurement says bass" in p.note
def test_near_silence_is_not_usable():
p = R.probe(np.zeros(SR * 5, dtype=np.float32), SR, claimed="drums")
assert not p.usable
def test_vox_and_tonal_do_not_contradict_each_other():
"""The cheap features cannot separate a sung line from a rhodes chord, so a vox
claim must not be overturned by a `tonal` measurement — that is CLAP's job."""
p = R.probe(_sine(440), SR, claimed="vox")
p.measured_family = "tonal"
p.agrees = True
assert R.effective_family(R.parse_stem_name("X MIX1 LV STEM.wav"), p) == "vox"
def test_a_hits_claim_survives_a_drums_measurement():
p = R.probe(_sine(60), SR, claimed="hits")
p.measured_family, p.agrees = "drums", False
name = R.parse_stem_name("X MIX1 KICK STEM.wav")
assert R.effective_family(name, p) == "hits"
# ── the kit audit ────────────────────────────────────────────────────────────
def _write(tmp, name, y, sr=SR):
import soundfile as sf
p = Path(tmp) / name
sf.write(str(p), np.stack([y, y], axis=1), sr, subtype="PCM_24")
return str(p)
def _cut(path, **kw):
d = dict(kit="k", name=Path(path).stem, path=path, track="T", stem_role="kit",
family="drums", mode="loop", bars=4, bpm=120.0, dur_s=0.0, start_s=0.0,
finder_score=1.0, tier="A", grade=0.85, flags=[], tags={})
d.update(kw)
return d
def _click_track(bars=4, bpm=120.0, dead_after=None):
"""A bar-exact percussive loop; `dead_after` silences every bar from that index."""
n = int(round(bars * 4 * 60.0 / bpm * SR))
y = np.zeros(n, dtype=np.float32)
per_beat = n // (bars * 4)
for b in range(bars * 4):
if dead_after is not None and b // 4 >= dead_after:
continue
i = b * per_beat
env = np.exp(-np.arange(per_beat) / (SR * 0.02))
y[i:i + per_beat] += (np.random.RandomState(b).randn(per_beat) * env * 0.3
).astype(np.float32)
return y
def test_a_bar_exact_loop_passes(tmp_path):
p = _write(tmp_path, "00_kit_4b.wav", _click_track())
(c,) = K.check_cuts([_cut(p)])
assert c.ok, c.problems
assert abs(c.bar_dev_ms) < K.BAR_TOL_MS
def test_a_loop_that_is_not_a_bar_multiple_is_caught(tmp_path):
y = _click_track()[: -SR // 10] # 100 ms short of 4 bars
p = _write(tmp_path, "01_kit_4b.wav", y)
(c,) = K.check_cuts([_cut(p)])
assert not c.ok
assert any("not a bar multiple" in x for x in c.problems)
def test_dead_bars_are_caught_even_though_the_loop_is_mechanically_perfect(tmp_path):
"""The whole reason this module exists: silence is a clean seam and an exact
length, so `grade` has no way to object to a loop that stops halfway."""
p = _write(tmp_path, "02_kit_4b.wav", _click_track(dead_after=2))
(c,) = K.check_cuts([_cut(p)])
assert not c.ok
assert any("dead bar" in x for x in c.problems)
def test_near_duplicates_within_a_kit_are_flagged(tmp_path):
y = _click_track()
a = _write(tmp_path, "00_alldrums_4b.wav", y)
b = _write(tmp_path, "01_kit_4b.wav", y * 0.9) # the same groove, another stem
checks = K.check_cuts([_cut(a), _cut(b)])
assert checks[1].dupe_of == checks[0].name
def test_distinct_material_is_not_flagged_as_duplicate(tmp_path):
rs = np.random.RandomState(0)
a = _write(tmp_path, "00_a_4b.wav", _click_track())
b = _write(tmp_path, "01_b_4b.wav", (rs.randn(int(4 * 4 * 0.5 * SR)) * 0.1).astype("float32"))
checks = K.check_cuts([_cut(a), _cut(b, bars=4, bpm=120.0)])
assert checks[1].dupe_of is None
def test_a_wrong_sample_rate_is_caught(tmp_path):
p = _write(tmp_path, "03_kit_4b.wav", _click_track(), sr=48000)
(c,) = K.check_cuts([_cut(p, bpm=130.909)])
assert not c.ok
assert any("sample rate" in x for x in c.problems)
def test_a_steady_tone_is_not_percussive_however_many_onsets_are_reported():
"""librosa's peak-picker finds peaks in a flat envelope — a steady 50 Hz sine
yields 84 onsets over six seconds (14/s). Without the crest gate that files a pure
bass tone as drums, which is how the bass override came to be unreachable."""
p = R.probe(_sine(50), SR, claimed="tonal")
assert p.onset_rate > 3.0 # the count really is that high
assert p.env_crest < 2.0 # and the envelope really is flat
assert p.measured_family == "bass"
def test_the_producers_label_survives_a_weak_contradiction():
"""MAREA's KEYS2 measures 5.1 onsets/s — genuinely rhythmic playing. Filing it as
drums would be silently wrong, so a known label wins and the disagreement is
reported instead."""
name = R.parse_stem_name("MAREA MIX10 123BPM KEYS2 STEM.wav")
p = R.probe(_sine(660), SR, claimed="tonal")
p.measured_family, p.agrees = "drums", False
assert R.effective_family(name, p) == "tonal"
def test_a_fallback_guess_loses_to_the_measurement():
"""`MAREA STEM` is not a role token; `tonal` there is our own guess, not the
producer's word, so it carries no authority against a measurement."""
name = R.parse_stem_name("MAREA MIX10 123BPM MAREA STEM.wav")
assert name.known is False
p = R.probe(_sine(660), SR, claimed="tonal")
p.measured_family, p.agrees = "drums", False
assert R.effective_family(name, p) == "drums"
def test_the_periodicity_floor_depends_on_the_family(tmp_path):
"""A drum loop that does not repeat at the bar is broken; a vocal phrase that does
not repeat within itself is a vocal phrase. One threshold for both condemned 12 of
20 vocal loops for being vocal."""
rs = np.random.RandomState(3)
y = (rs.randn(int(4 * 4 * 0.5 * SR)) * 0.05).astype("float32") # no bar period
p = _write(tmp_path, "00_x_4b.wav", y)
(drums,) = K.check_cuts([_cut(p, family="drums", bars=4, bpm=120.0)])
(vox,) = K.check_cuts([_cut(p, family="vox", bars=4, bpm=120.0)])
assert any("periodicity" in x for x in drums.problems)
assert not any("periodicity" in x for x in vox.problems)
def test_periodicity_does_not_penalise_a_short_loop_for_being_short():
"""At lag k only N-k frames overlap, so normalising by ac[0] scores identical
material lower the shorter the loop: measured 0.418/0.623/0.754 at 2/4/8 bars on a
perfect click track. The unbiased estimator has to come out flat."""
scores = [K.periodicity(_click_track(bars=b), SR, b) for b in (2, 4, 8)]
assert min(scores) > 0.5 # all read as clearly periodic
assert max(scores) - min(scores) < 0.25 # and length is not what decides it
def test_a_loop_that_fades_out_halfway_never_ships(tmp_path):
"""The export gate, not just the audit: grade cannot object to this (silence is a
perfect seam and the length is exact) so it has to be caught before it is written."""
import engine.stempack as SP
clip = _click_track(dead_after=2)[:, None]
assert SP._has_dead_bars(clip, SR, 4)
assert not SP._has_dead_bars(_click_track()[:, None], SR, 4)
def test_headroom_is_one_gain_for_the_whole_kit_and_never_boosts():
"""Per-file normalisation would make a sparse hat loop as loud as a full drum bus
and throw away the balance two loops from one kit otherwise have for free. And a
quiet kit is quiet on purpose — lifting it amplifies its noise floor."""
import engine.stempack as SP
hot = SP.kit_headroom_gain([0.2, -3.0, -12.0])
assert 20 * np.log10(hot) == pytest.approx(-6.2, abs=0.05)
assert SP.kit_headroom_gain([-15.0, -20.0]) == 1.0 # never boosts
assert SP.kit_headroom_gain([-6.0]) == 1.0 # already at the ceiling
assert SP.kit_headroom_gain([]) == 1.0
def test_headroom_preserves_relative_levels_within_a_kit():
import engine.stempack as SP
peaks = [0.0, -6.0, -18.0]
g = SP.kit_headroom_gain(peaks)
after = [p + 20 * np.log10(g) for p in peaks]
assert max(after) == pytest.approx(-6.0, abs=0.05)
# the spacing between the loops is untouched, which is the whole point
assert [a - after[0] for a in after] == pytest.approx([p - peaks[0] for p in peaks])
def test_a_staged_kit_records_the_grade_of_what_it_actually_wrote(tmp_path, monkeypatch):
"""kitgate compares the manifest against a fresh grade of the file. If staging moves
the audio after grading, that comparison is between two different signals and the
difference gets reported as a defect — which is how this bug was found."""
import engine.stempack as SP
from engine import grade as G
y = _click_track()
y = y / (np.max(np.abs(y)) + 1e-9) # full scale ⇒ staging will engage
clip = np.stack([y, y], axis=1)
head = SP.kit_headroom_gain([0.0])
assert head < 1.0
before = G.grade_array(clip.T, SR).grade
after = G.grade_array((clip * head).T, SR).grade
assert abs(before - after) > 0.02 # the grade really does move
# ── the gate's own listing check ─────────────────────────────────────────────
def _kitgate():
import importlib.util
spec = importlib.util.spec_from_file_location("kitgate", Path(__file__).parent.parent / "kitgate.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
def test_a_clean_kit_dir_matches_its_manifest(tmp_path):
cuts = [_cut(_write(tmp_path, f"{i:02d}_kit_4b.wav", _click_track())) for i in range(3)]
ok, lines = _kitgate().check_indices(cuts)
assert ok, lines
def test_a_leftover_from_an_earlier_cut_shifts_every_index_after_it(tmp_path):
"""The failure this check exists for: export writes into a kit dir it never empties,
so re-cutting `00_kit_2b` as `00_kit_4b` leaves BOTH on disk. SuperDirt enumerates
the folder, so `n 1` now plays what the cheat sheet calls `n 0` — and nothing about
the manifest, the grades or the audio looks wrong."""
cuts = [_cut(_write(tmp_path, f"{i:02d}_kit_4b.wav", _click_track())) for i in range(3)]
_write(tmp_path, "00_kit_2b.wav", _click_track(bars=2)) # the stale leftover
g = _kitgate()
ok, lines = g.check_indices(cuts)
assert not ok
assert "orphan" in " ".join(lines)
assert "n 0 onward" in " ".join(lines) # it says WHERE the addressing breaks
assert g.prune_orphans(cuts) == 1
assert g.check_indices(cuts)[0]
def test_an_orphan_that_sorts_last_is_still_reported_but_indices_hold(tmp_path):
"""A trailing leftover pollutes the bank without moving any index — a different
severity, and the report has to say which one it is."""
cuts = [_cut(_write(tmp_path, f"{i:02d}_kit_4b.wav", _click_track())) for i in range(3)]
_write(tmp_path, "09_kit_4b.wav", _click_track())
ok, lines = _kitgate().check_indices(cuts)
assert not ok
assert "indices still line up" in " ".join(lines)
# ── restaging is idempotent ──────────────────────────────────────────────────
def _restage():
import importlib.util
spec = importlib.util.spec_from_file_location("restage", Path(__file__).parent.parent / "restage.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
def test_restaging_a_staged_kit_is_a_no_op(tmp_path):
"""Export stages in float and writes PCM_24, so the peak read back off disk sits a
quantization step away from the ceiling it aimed at — which read as "still too hot"
and asked for another -0.00003 dB, every run, requantizing every file each time."""
r = _restage()
y = _click_track()
y = y / (np.max(np.abs(y)) + 1e-9)
_write(tmp_path, "00_kit_4b.wav", y)
_write(tmp_path, "01_kit_4b.wav", y * 0.5)
first = r.restage_kit(tmp_path)
assert first["changed"] and first["gain_db"] == pytest.approx(-6.0, abs=0.05)
second = r.restage_kit(tmp_path)
assert not second["changed"], second # the run that used to loop forever
assert r.restage_kit(tmp_path, dry_run=True)["changed"] is False
def test_a_kit_that_really_is_too_hot_is_still_restaged(tmp_path):
"""The dead band must not swallow a gain that matters — 0.05 dB, not 0.5."""
r = _restage()
y = _click_track()
_write(tmp_path, "00_kit_4b.wav", y / (np.max(np.abs(y)) + 1e-9) * 0.9)
out = r.restage_kit(tmp_path, dry_run=True)
assert out["changed"] and out["gain_db"] < -5.0
# ── silence is not a loop ────────────────────────────────────────────────────
def test_a_silent_loop_passes_every_structural_test_and_is_still_rejected(tmp_path):
"""The deepest version of the dead-bar trap. A window of pure silence is bar-exact,
has a perfect seam, zero DC, trivial zero crossings — and a perfectly FLAT bar
profile, so the dead-bar check (which compares bars to each other) sees nothing
wrong. 24 of the 153 loops cut from the Fred pack were digital silence at -104…-75
dBFS peak, graded 0.707-0.896, tier A and B."""
sr = SR
n = int(round(4 * 4 * 60.0 / 120.0 * sr))
quiet = (np.random.RandomState(0).randn(n) * 1e-5).astype(np.float32) # ~ -95 dBFS
p = _write(tmp_path, "00_bass_4b.wav", quiet)
lv = K.bar_levels(quiet, sr, 4)
assert max(lv) - min(lv) < K.DEAD_BAR_DB # the old check sees a clean loop
(c,) = K.check_cuts([_cut(p, family="bass")])
assert not c.ok
assert any("empty" in x for x in c.problems), c.problems
def test_the_two_presence_lenses_use_one_number():
"""kitcheck and grade both decide 'is there audio here'. If they used two constants
one would eventually be edited alone, and the gate would pass what the grader
failed."""
from engine import grade as G
assert K.EMPTY_DBFS == G.THRESH["empty_dbfs"]
# ── a background job has to behave like one ──────────────────────────────────
def test_the_default_job_count_leaves_the_machine_usable():
"""PLN performs and edits on this machine. Nine children at ~88% of a core each took
the load average to 15.6 on 16 cores and made the desktop unusable — so the default
is half the cores, capped, rather than one per track."""
import os
import engine.stempack as SP
n = len(os.sched_getaffinity(0))
j = SP._default_jobs()
assert 1 <= j <= 6
assert j <= max(1, n // 2)
def test_children_are_started_at_idle_priority():
"""Assert on a real child, not on the hook's existence. `_fan_out` passes this as
subprocess's preexec hook, so if it silently did nothing the run would still work —
it would just take the desktop down with it, which is how this was noticed."""
import os
import subprocess
import sys
import engine.stempack as SP
src = "import os;print(os.nice(0), os.sched_getscheduler(0))"
out = subprocess.run([sys.executable, "-c", src], capture_output=True, text=True,
preexec_fn=SP.run_at_idle_priority)
assert out.returncode == 0, out.stderr
nice, policy = out.stdout.split()
assert int(nice) == 19, f"child ran at nice {nice}"
assert int(policy) == os.SCHED_IDLE, f"child policy {policy}, wanted SCHED_IDLE"
# ── the gate blocks on defects, not on properties ────────────────────────────
def _gate_flags(cut, flags, family=None):
"""Run kitgate's blocking-flag rule against a fabricated grade result."""
g = type("G", (), {"flags": flags, "metrics": {}})()
return _kitgate()._blocking_flags({**cut, "family": family or cut.get("family")}, g)
def test_a_quiet_or_wide_sample_does_not_block_the_pack(tmp_path):
"""The first version of this gate blocked all 150 samples on 20 `near-silent` and 17
`mono-incompatible` files, none of which was broken. A kit is gain-staged as a unit
and never boosted, so its quiet members are quiet ON PURPOSE, and low interchannel
correlation in a vox double is the width someone paid for. Blocking on a property is
not the safe direction — it is a gate nobody can pass and will learn to ignore."""
c = _cut("/x/00_bv_1b.wav", family="vox")
assert _gate_flags(c, ["near-silent"]) == []
assert _gate_flags(c, ["mono-incompatible"]) == []
assert _gate_flags(c, ["seam-click"]) == []
def test_clipping_and_emptiness_do_block(tmp_path):
c = _cut("/x/00_kit_4b.wav")
assert _gate_flags(c, ["clipping"]) == ["clipping"]
assert _gate_flags(c, ["empty"]) == ["empty"]
def test_mono_collapse_blocks_on_bass_only():
"""Summed to mono on a house PA a wide bass loses its fundamental, which nobody
chose. The same measurement on a pad is the sound of the pad."""
assert _gate_flags(_cut("/x/00_bass_2b.wav"), ["mono-incompatible"], family="bass")
assert _gate_flags(_cut("/x/00_pad_2b.wav"), ["mono-incompatible"], family="tonal") == []
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment