Commit 672b0fb3 by PLN (Algolia)

fix(foundry): the loops have holes in them — an aggregate cannot see a hole

PLN opened fred_angie_bass, the first kit he tried, and found two bad loops
immediately: ":0 starts, then silence, then starts again" and ":2 why is this
not trimmed, silence before silence after?". Both had shipped as tier S with
zero flags, one at grade 0.991.

He was right, and it is worse than two files. Binning each loop's envelope on
its own beat grid says 12 of 111 loops carry dead air — and 5 of 14 bass loops,
36%, which is why he hit it on his first click. angie:0 has beats 7-9 of 32 at
-88 dBFS; bighen_bass:1 has a five-beat hole and 62.5% occupancy at grade 0.983.

This is the third time this session that silence beat the grader, and the
reason is the same each time, one level further down. Silence-as-optimum was
fixed at export, then again in the finder before ranking, but every one of those
checks is an AGGREGATE: file peak, whole-file RMS, max of the beat table. An
aggregate cannot see a hole. A loud loop with a silent bar inside it passes all
three. And kitcheck's dead-bar rejector looks at BARS, so a 2.5-beat gap
straddling a bar boundary is invisible — angie:0's two bars read -18.3 and
-15.9 dBFS while the beats between them read -88. Resolution was a correctness
input and I had treated it as a detail.

So this commit does not fix it, it measures it. beatocc.py bins the envelope on
the loop's own beat grid (bpm and bars from the manifest, never re-estimated)
and reports SHAPE rather than level: head and tail dead beats, longest interior
dead run, occupancy, plus a beat map under -v. It exits non-zero when any loop
has dead air, so it can go straight into kitgate once the thresholds are known.

Thresholds deliberately not chosen yet. Occupancy floors have to be per family
like the periodicity floors — marea_vox/06_breaths_1b at 25% occupancy is a
breath, not a defect, while a bass loop at 62% is broken — and the head-silence
question has three plausible answers (trim, rotate, reject) with rotation ruled
out already, since angie:2's offset is 1.17 beats and _rotate_to_downbeat snaps
to beats. Placing those floors by guess is how silence got graded A in the first
place. #24 on the board carries the open decisions and the data needed for each.

The Fred pack section is now marked DEFECTIVE rather than SHIPPED. It needs a
re-cut once the shape check exists, and PLN's verdict stands as written:
"tbh i dont think the loops are usable."
parent 830320db
...@@ -56,7 +56,9 @@ python3 -m engine.stempack "<pack>" --jobs 9 --no-link --out /tmp/cut ...@@ -56,7 +56,9 @@ python3 -m engine.stempack "<pack>" --jobs 9 --no-link --out /tmp/cut
python3 kitgate.py /tmp/cut/fred_kits.json # then --link python3 kitgate.py /tmp/cut/fred_kits.json # then --link
``` ```
## 🟢 Landed (2026-08-22) — the Fred pack SHIPPED ## 🟡 Landed but DEFECTIVE (2026-08-22) — the Fred pack shipped with holes in it
> **Do not treat this pack as good.** See **#24** below: 12 of 111 loops contain dead
> air the grade never looked for. PLN found two of them in the first kit he opened.
150 samples · 43 kits · 9 tracks, staged at `~/Work/Sound/Samples/Fred/output/<kit>/` 150 samples · 43 kits · 9 tracks, staged at `~/Work/Sound/Samples/Fred/output/<kit>/`
(PLN's choice) and symlinked into Dirt-Samples. `s "fred_marea_drums" # n 3` resolves. (PLN's choice) and symlinked into Dirt-Samples. `s "fred_marea_drums" # n 3` resolves.
...@@ -118,6 +120,61 @@ swing control wired to the UI though the engine supports it; and `?kits=fred_*` ...@@ -118,6 +120,61 @@ swing control wired to the UI though the engine supports it; and `?kits=fred_*`
the rail but there is no way to save a rack as a named preset. the rail but there is no way to save a rack as a named preset.
## 🔴 Open ## 🔴 Open
- [ ] **#24 — Loops have dead air inside them; every presence check is an AGGREGATE.**
**PLN's verdict, 2026-08-22: "tbh i dont think the loops are usable."** He opened
one kit and found two defects, both graded **tier S with zero flags**:
- `fred_angie_bass:0` (`00_bass_8b`, grade **0.991**) — *"it starts, then silence,
then starts again"*. Beats 7-9 of 32 are **digital silence** (-52, -88, -88 dBFS).
Beat map: `#######..#######################`
- `fred_angie_bass:2` (`02_bass_2b`, grade **0.920**) — *"why is this not trimmed,
silence before silence after?"* 528 ms head (1.17 beats) + 436 ms tail dead.
The bass enters on beat 2, so `loopAt 2` puts silence on the downbeat.
Beat map: `.#######`
**Not isolated — 12 of 111 loops (11%), and 5 of 14 bass loops (36%):**
fam hit/total the three shapes
bass 5/14 35.7% head>=1 beat: 4 tail>=1 beat: 5 interior gap>=2 beats: 5
drums 4/45 8.9% worst: bighen_bass/01_bass_8b gap=5 beats, occ 62.5%, S 0.983
vox 2/24 8.3% yasminah_drums/07_hidrums_1b head=3 of 4, occ 25%, S 0.909
tonal 1/20 5.0% dermot_bass/00_bass_8b gap=4, occ 87.5%, S 0.948
fx 0/8 0.0%
**Root cause — the same bug one level down.** Silence-graded-A was fixed twice this
session (at export, then before ranking) but *both* fixes are aggregates: file peak,
whole-file RMS, `max(beat_peak)`. **An aggregate cannot see a hole.** A loud loop with
a silent bar inside it passes every one of them. The dead-bar rejector in `kitcheck`
has **bar** resolution, so a 2.5-beat hole straddling a bar boundary is invisible —
angie:0's bars read -18.3 and -15.9 dBFS while the beats between them read -88.
**The lens exists already**: `loops._beat_peaks` builds the exact per-beat table.
New diagnostic committed: **`tools/foundry/beatocc.py`** — bins the envelope on the
loop's own beat grid (bpm/bars from the manifest, never re-estimated) and reports
*shape* (head/tail dead beats, longest interior run, occupancy) instead of level:
cd tools/foundry
python3 beatocc.py packs/fred/fred_kits.json # the 12
python3 beatocc.py packs/fred/fred_kits.json --kit fred_angie_bass -v # beat maps
**Open decisions — needs data, do not guess:**
1. **Occupancy floors must be PER FAMILY**, exactly like the periodicity floors.
`marea_vox/06_breaths_1b` at 25% occupancy is a *breath*, not a defect; a bass
loop at 62.5% is broken. Measure the medians before placing a floor.
2. **Head/tail silence: trim, rotate, or reject?** Rotation can't fix angie:2 — the
offset is 1.17 beats and `_rotate_to_downbeat` snaps to beats, and rolling only
moves the hole to the end. Trimming changes the length so it's no longer
`loopAt`-able at an integer bar count. Rejecting may be the honest answer.
3. **Interior gaps: reject, or accept as a musical rest?** A 2-beat rest in a bassline
is real music; 5 beats of nothing in an 8-bar loop is a bad window. The finder's
window ranking should probably *penalise* dead beats rather than the gate rejecting
them outright — that puts it in #19's weight-tuning loop.
4. Whichever way it lands, `kitcheck`'s dead-bar check must move from **bar** to
**beat** resolution, and `kitgate` must gain a shape check. Then **re-cut Fred**
(`packs/fred/` has the exact command block above) and re-audition.
Lesson banked: `feedback_presence_is_a_precondition`, `feedback_measure_the_time_axis`.
- [ ] **#22 — Point stempack at the rest.** Same driver, no new code: - [ ] **#22 — Point stempack at the rest.** Same driver, no new code:
the 13 ACAPELLAS + 13 INSTRUMENTALS in `~/Work/Sound/Samples/Fred/` (already 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 44.1 kHz, so no resample; acapellas want chops mode), and the ~27 demucs catches in
......
#!/usr/bin/env python3
"""Beat-resolution occupancy: where inside a loop is there nothing?
The presence checks in `grade`/`loops` are AGGREGATES over the whole window
(file peak, whole-file RMS, max of the beat table). An aggregate cannot see a
HOLE. `fred_angie_bass:0` graded 0.991 / tier S with 2.5 beats of digital
silence (-88 dBFS) sitting at beats 7-9 of 32 — PLN heard "it starts, then
silence, then starts again" the moment he auditioned it.
So: bin the envelope on the loop's OWN beat grid (bpm + bars come from the
manifest, never re-estimated) and report the SHAPE, not the level —
head/tail dead beats and the longest interior dead run.
python3 beatocc.py packs/fred/fred_kits.json # whole pack
python3 beatocc.py packs/fred/fred_kits.json --kit fred_angie_bass -v
Dead = beat peak below --rel dB relative to that loop's own peak (default -40),
which is a shape measure and so must stay relative; an absolute floor would
just re-measure level. Occupancy floors are NOT applied here on purpose:
they belong per family, like the periodicity floors (a breath loop at 25%
occupancy is a breath loop, not a defect). This tool reports; the gate decides.
"""
from __future__ import annotations
import argparse, json, sys
from pathlib import Path
import numpy as np
import soundfile as sf
def beat_occupancy(path: Path, bpm: float, dur_s: float, rel_db: float = -40.0):
"""-> (n_beats, head_dead, tail_dead, longest_interior_run, occupancy, live[])"""
y, sr = sf.read(str(path), always_2d=True)
a = np.abs(y).max(axis=1) # channel max, never the mono sum
if not a.size or a.max() <= 0:
return 0, 0, 0, 0, 0.0, np.zeros(0, bool)
beat = 60.0 / bpm
n = max(1, int(round(dur_s / beat)))
thr = a.max() * 10 ** (rel_db / 20.0)
live = np.array([
bool(a[int(i * beat * sr):int((i + 1) * beat * sr)].max() > thr)
if int((i + 1) * beat * sr) > int(i * beat * sr) else False
for i in range(n)])
if not live.any():
return n, n, n, n, 0.0, live
head = int(np.argmax(live))
tail = int(np.argmax(live[::-1]))
run = cur = 0
for v in live[head:n - tail]:
cur = 0 if v else cur + 1
run = max(run, cur)
return n, head, tail, run, float(live.sum()) / n, live
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("manifest", type=Path)
ap.add_argument("--kit", action="append", default=[], help="restrict to kit(s)")
ap.add_argument("--rel", type=float, default=-40.0, help="dead threshold, dB rel. peak")
ap.add_argument("-v", "--verbose", action="store_true", help="print the beat map")
a = ap.parse_args()
doc = json.loads(a.manifest.read_text())
cuts = doc["cuts"] if isinstance(doc, dict) else doc
cuts = [c for c in cuts if c.get("mode") == "loop"
and (not a.kit or c.get("kit") in a.kit)]
rows, missing = [], 0
for c in cuts:
p = Path(c["path"])
if not p.exists():
missing += 1
continue
n, head, tail, run, occ, live = beat_occupancy(p, c["bpm"], c["dur_s"], a.rel)
rows.append((c, n, head, tail, run, occ))
if a.verbose:
print(f"{c['kit']}/{c['name']} {c['bpm']:.2f}bpm {n}bt "
f"head={head} tail={tail} gap={run} occ={occ*100:.1f}%")
print(" " + "".join("#" if v else "." for v in live))
print(f"\n{len(rows)} loops · dead = beat peak < {a.rel:+.0f} dB rel. to its own peak"
+ (f" · {missing} missing on disk" if missing else ""))
flagged = [r for r in rows if r[2] >= 1 or r[3] >= 1 or r[4] >= 2]
if flagged:
print(f"\n{len(flagged)} of {len(rows)} loops have dead air the grade did not see:\n")
print(f" {'kit':<26} {'name':<18} {'fam':<6} beats head tail gap occ tier grade")
for c, n, head, tail, run, occ in sorted(flagged, key=lambda r: -max(r[2], r[3], r[4])):
print(f" {c['kit']:<26} {c['name']:<18} {c.get('family',''):<6} "
f"{n:>5} {head:>4} {tail:>4} {run:>3} {occ*100:>5.1f}% "
f"{c.get('tier','?')} {c.get('grade',0):.3f}")
else:
print("\nEvery loop is occupied edge to edge.")
return 1 if flagged else 0
if __name__ == "__main__":
sys.exit(main())
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