Commit 75e4ec16 by PLN (Algolia)

feat(foundry): stage a kit's level, because amp = gain^4

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. grade already calls that
clipping, and SuperDirt cannot use it: amp = gain^4, so `# gain 1.2` is +7.6 dB and the
orbit clips the moment you touch it. The house rule is peaks at -6 dBFS at the source and
let the amplification happen downstream, so that is where kits land now.

Two decisions inside kit_headroom_gain that matter more than the ceiling:

NOT per file. One scalar for the whole kit. Normalising each file on its own would make a
sparse hat loop exactly as loud as a full drum bus, throwing away the balance that two
loops from one kit otherwise have for free — the reason to cut a kit from one track in
the first place.

NEVER boosts. A kit already below the ceiling is left exactly where it is. A quiet stem
is quiet on purpose; lifting it amplifies its noise floor and discards real information
about the arrangement.

restage.py applies the same function to kits already on disk, so a kit restaged after the
fact is bit-identical to one cut with it in place. Useful beyond this pack: hand-cut kits
carry whatever level their source had, and there are 163 kits in Samples/.

It also re-grades the manifest for anything it changed. Whoever moves the audio owns the
record of it — every grade has a level term, so a stale manifest describes bytes that no
longer exist, and kitgate compares the two precisely to catch that drift. Verified on the
synthetic pack: clipping cleared, two drum loops A -> S, and the gate then caught the
stale grades until restage started writing them back.

23 tests in the file.
parent e6e0ab78
......@@ -336,11 +336,14 @@ def export_track(result: dict, *, prefix: str, samples_root: Optional[Path] = No
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"
sf.write(str(dest), clip, ANALYSIS_SR, subtype="PCM_24")
sf.write(str(dest), clip * head, ANALYSIS_SR, subtype="PCM_24")
written.append(dest)
out.append(CutLoop(
kit=kit, name=dest.stem, path=str(dest), track=result["dir"].name,
......@@ -356,7 +359,8 @@ def export_track(result: dict, *, prefix: str, samples_root: Optional[Path] = No
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"
progress(f" → {kit:34.34} {len(written):2} files{bad}")
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)
......@@ -366,6 +370,37 @@ def export_track(result: dict, *, prefix: str, samples_root: Optional[Path] = No
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.
......
#!/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.
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
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:
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())
......@@ -10,6 +10,7 @@ import sys
from pathlib import Path
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
......@@ -224,3 +225,25 @@ def test_a_loop_that_fades_out_halfway_never_ships(tmp_path):
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])
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