Commit edb62258 by PLN (Algolia)

feat(foundry): the loop grader (engine/grade.py) — the katana (#10)

A pure grade(wav) -> LoopGrade that scores ANY sample against the empirical
A-profile + the B mechanical rulebook (PHASE2_FINDINGS §2-§3, §5a): seam click,
zero-crossing cleanliness, DC offset, bar-length self-consistency, level sanity,
bass mono-compatibility, and the one-shot/loop class. Composite 0-1 → S/A/B/C/D
tier + per-rule sub-scores + human-readable flags. This is step 1 of the phase-2
build order — the instrument the finder (#5) and the Hall of Fame tierlist (#11)
both reuse (DRY, feedback_build_katana_first): the finder grades hypothetical
windows, the tierlist grades existing files, same sub-scorers.

Two findings while validating against real ParVagues loops (crimewave/diams_dj/humpty):

1. bar-consistency was DEAD — `librosa.feature.rhythm.tempo` doesn't exist in
   librosa 0.11 (it's `librosa.feature.tempo`), so a swallowed AttributeError sent
   every file down the no-tempo fallback → a flat 0.50 for all. Fixed; now crimewave
   drums correctly reads as 4 bars @ 121.9 bpm (matches the GT note ~121.8).

2. seam was measuring the single-sample wrap step |x0-x[-1]| vs the signal's MEAN
   step — which unfairly punishes a loop for wrapping at a zero crossing (the
   steepest point: a *perfect* integer-period sine scored only 0.40). §3 wants
   waveform CONTINUITY, so seam now measures the second difference (jerk) at the
   wrap, |x0 - 2·x[-1] + x[-2]|, normalized by typical curvature — captures both a
   value jump and a slope break, and a clean wrap now grades ~1.0.

Sanity: crimewave (a core-palette kit) lands mostly B-tier or above, with clipping
/off-grid cuts correctly sunk to D — consistent with §5b's "core kits must tier high"
validation target. The composite WEIGHTS + THRESH are tagged v1/provisional: the
dc-offset flag is still noisy (1e-4 is spec-strict) and the seam dB cutoffs want
calibrating — that's #11's job (corpus-wide grade.tidal-usage correlation), not
eyeball-tuning on 3 kits. 28 tests pass (synthetic signals, relative assertions).

Runs under system python3 (numpy/scipy/soundfile/librosa already present); no GPU,
no network. CLI: python3 -m engine.grade <wav>… [--json].
parent 1a201dfa
"""grade — the loop grader (the katana, task #10).
A pure `grade(wav) -> LoopGrade` that applies the empirical A-profile + the B
mechanical rulebook (`research/PHASE2_FINDINGS.md` §2–§3, §5a) to ANY sample. It
answers one question per file: *how good a loop is this, mechanically?* — seam
click, zero-crossing cleanliness, DC offset, bar-length self-consistency, level
sanity, bass mono-compatibility, and the one-shot/loop class.
Two consumers share this code (DRY, `feedback_build_katana_first`):
• the **tierlist** (task #11) grades existing `Samples/**/*.wav`;
• the **finder** (task #5) grades hypothetical windows — it reuses the same
sub-scorers (`seam_score`, `zc_score`, …) on candidate slices, then adds the
structural/novelty terms it gets from its SSM context.
Runs under system python3 (numpy/scipy/soundfile/librosa already present). No
network, no GPU. The composite weights + thresholds are **v1, provisional** —
validation is task #11 (grade↔.tidal-usage correlation: core-palette kits must
tier high) and task #7. Tune `WEIGHTS`/`THRESH` there, not by guessing here.
python3 -m engine.grade path/to/loop.wav [more.wav …] # → grades as JSON
"""
from __future__ import annotations
import math
from pathlib import Path
from typing import Optional
import numpy as np
import soundfile as sf
from pydantic import BaseModel, Field
# ── tunables (v1; calibrate in #11/#7, see module docstring) ─────────────────
THRESH = {
"one_shot_s": 0.75, # B9: below this it's a one-shot, not a loop
"dc_ok": 1e-4, # B2: |mean| above this is flagged
"dc_zero": 1e-2, # |mean| at which the DC score bottoms out
"seam_db_clean": -20, # wrap step this far below natural motion ⇒ clean seam
"seam_db_click": 20, # wrap step this far above natural motion ⇒ audible click
"zc_tol_ms": 10.0, # B1: ±10 ms snap tolerance
"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
"bars": (1, 2, 4, 8), # B-rule bar interpretations to test
"beats_per_bar": 4,
"tempo_lo": 60.0,
"tempo_hi": 200.0,
}
# Composite weights. Loops and one-shots are graded on different criteria — a
# one-shot is never looped, so seam/bar self-consistency don't apply to it.
WEIGHTS = {
"loop": {"seam": 0.35, "bar": 0.25, "zc": 0.15, "level": 0.10, "dc": 0.10, "bass_mono": 0.05},
"one_shot": {"level": 0.5, "dc": 0.3, "zc": 0.2},
}
TIERS = [("S", 0.90), ("A", 0.80), ("B", 0.70), ("C", 0.55), ("D", 0.0)]
_EPS = 1e-12
def _clamp01(x: float) -> float:
return 0.0 if x < 0 else 1.0 if x > 1 else float(x)
def _db(x: float) -> float:
return 20.0 * math.log10(max(abs(x), _EPS))
# ── data model ───────────────────────────────────────────────────────────────
class LoopGrade(BaseModel):
"""A mechanical loop-quality grade for one audio file."""
path: str
kind: str # "loop" | "one_shot"
grade: float # composite 0..1
tier: str # S|A|B|C|D
sub: dict[str, float] # per-rule sub-scores, each 0..1
metrics: dict[str, float] # raw measured values (dur, dbfs, bpm, …)
flags: list[str] = Field(default_factory=list) # human-readable defects
def tier_of(self) -> str:
for name, lo in TIERS:
if self.grade >= lo:
return name
return "D"
# ── audio loading ─────────────────────────────────────────────────────────────
def load_audio(path: str | Path) -> tuple[np.ndarray, int]:
"""Read any soundfile-supported file → (channels, n) float64, native sr."""
y, sr = sf.read(str(path), always_2d=True, dtype="float64")
return y.T, int(sr) # (n, ch) → (ch, n)
def _mono(y: np.ndarray) -> np.ndarray:
return y if y.ndim == 1 else y.mean(axis=0)
# ── pure sub-scorers (reused by the finder on candidate slices) ───────────────
def dc_score(mono: np.ndarray) -> tuple[float, float]:
"""B2 — DC offset. Returns (score 0..1, raw mean)."""
dc = float(np.mean(mono))
a = abs(dc)
if a <= THRESH["dc_ok"]:
return 1.0, dc
span = THRESH["dc_zero"] - THRESH["dc_ok"]
return _clamp01(1.0 - (a - THRESH["dc_ok"]) / span), dc
def seam_score(mono: np.ndarray) -> tuple[float, float]:
"""Loop the file head-to-tail; measure the wrap click as a continuity defect.
§3 wants *waveform continuity* at the seam, not a single-sample diff. The
click is a discontinuity in the signal's TRAJECTORY at the wrap: does x[0]
continue smoothly from x[-1], x[-2]? The second difference there —
|x[0] − 2·x[-1] + x[-2]| — is the curvature/jerk of the wrap, and it captures
both a value jump and a slope break. Crucially it does NOT punish a loop for
wrapping at a zero crossing (the steepest point) the way a step-vs-mean ratio
would. Normalized by the signal's typical curvature. Returns (score 0..1,
seam-jerk dB above typical motion): very negative ⇒ seamless.
"""
if mono.size < 3:
return 1.0, -60.0
# curvature at the wrap: the sequence is … x[-2], x[-1], x[0], x[1] …
jerk = abs(float(mono[0]) - 2.0 * float(mono[-1]) + float(mono[-2]))
typical = float(np.mean(np.abs(np.diff(mono, n=2)))) + _EPS # typical |2nd diff|
click_db = _db(jerk / typical)
clean, click = THRESH["seam_db_clean"], THRESH["seam_db_click"]
# ≤ clean dB (jerk ≈ natural curvature) ⇒ 1.0 ; ≥ click dB (audible) ⇒ 0
score = _clamp01((click - click_db) / (click - clean))
return score, click_db
def _zero_crossings(mono: np.ndarray) -> np.ndarray:
# sign flips, robust to exact zeros (signbit treats 0 as positive)
return np.where(np.diff(np.signbit(mono)))[0]
def zc_score(mono: np.ndarray, sr: int) -> tuple[float, float, float]:
"""B1 — endpoint zero-crossing cleanliness.
Combines two facts: how near the endpoints sit to zero (a clean loop ends at
a crossing) and how close a real ZC is to snap to. Returns
(score 0..1, start dist ms, end dist ms).
"""
peak = float(np.max(np.abs(mono))) + _EPS
end_amp = (abs(float(mono[0])) + abs(float(mono[-1]))) / (2 * peak)
zcs = _zero_crossings(mono)
if zcs.size:
d0 = float(np.min(np.abs(zcs - 0))) / sr * 1000.0
d1 = float(np.min(np.abs(zcs - (mono.size - 1)))) / sr * 1000.0
else:
d0 = d1 = THRESH["zc_tol_ms"] * 4
# within tol ⇒ snappable; amplitude-at-ends near zero ⇒ already clean
snap = 1.0 - min(1.0, ((d0 + d1) / 2) / (THRESH["zc_tol_ms"] * 2))
score = _clamp01(0.5 * (1.0 - end_amp) + 0.5 * snap)
return score, d0, d1
def level_score(mono: np.ndarray) -> tuple[float, float, float, bool]:
"""A-profile level sanity + clip guard. Returns (score, rms dBFS, peak dBFS, clipped)."""
rms = math.sqrt(float(np.mean(mono ** 2)) + _EPS)
peak = float(np.max(np.abs(mono))) + _EPS
rms_db, peak_db = _db(rms), _db(peak)
lo, hi = THRESH["rms_lo_dbfs"], THRESH["rms_hi_dbfs"]
if lo <= rms_db <= hi:
score = 1.0
elif rms_db < lo: # too quiet → 0 by -60 dBFS
score = _clamp01(1.0 - (lo - rms_db) / (60.0 + lo))
else: # too hot → 0.4 by 0 dBFS
score = _clamp01(1.0 - 0.6 * (rms_db - hi) / (0.0 - hi))
clipped = peak_db >= THRESH["clip_dbfs"]
if clipped:
score *= 0.5
return score, rms_db, peak_db, clipped
def bass_mono_score(y: np.ndarray) -> tuple[float, float]:
"""B6 — inter-channel correlation (mono-sum safety). Mono files → 1.0 (N/A)."""
if y.ndim == 1 or y.shape[0] < 2:
return 1.0, 1.0
a, b = y[0], y[1]
if np.std(a) < _EPS or np.std(b) < _EPS:
return 1.0, 1.0
corr = float(np.corrcoef(a, b)[0, 1])
return _clamp01((corr + 1.0) / 2.0), corr
def bar_consistency_score(mono: np.ndarray, sr: int) -> tuple[float, dict]:
"""§3 tempo self-check — does the duration map to a clean {1,2,4,8}-bar count
at a musical tempo? Returns (score 0..1, {detected_bpm, best_bars, implied_bpm, tempo_err}).
"""
dur = mono.size / sr
info = {"detected_bpm": 0.0, "best_bars": 0, "implied_bpm": 0.0, "tempo_err": 1.0}
if dur <= 0:
return 0.0, info
detected = 0.0
try: # librosa optional/slow — guard it
import librosa
t = librosa.feature.tempo(y=mono.astype(np.float32), sr=sr, aggregate=np.median)
detected = float(np.atleast_1d(t)[0])
except Exception:
detected = 0.0
info["detected_bpm"] = round(detected, 2)
bpb = THRESH["beats_per_bar"]
best = None
for bars in THRESH["bars"]:
implied = bars * bpb * 60.0 / dur
if not (THRESH["tempo_lo"] <= implied <= THRESH["tempo_hi"]):
continue
if detected > 0: # compare across ±octave
err = min(abs(implied - detected * f) / (detected * f) for f in (0.5, 1, 2))
else: # no tempo read → reward musical range only
err = 0.05
if best is None or err < best[2]:
best = (bars, implied, err)
if best is None: # no interpretation lands in-range
return 0.0, info
bars, implied, err = best
info.update(best_bars=bars, implied_bpm=round(implied, 2), tempo_err=round(err, 4))
score = _clamp01(1.0 - err / 0.10) # ±2% ≈ 0.8 ; ≥10% ⇒ 0
return score, info
# ── the grader ────────────────────────────────────────────────────────────────
def grade_array(y: np.ndarray, sr: int, *, path: str = "<array>",
role: Optional[str] = None) -> LoopGrade:
"""Grade an in-memory signal (ch, n) or (n,). `role` ('bass'…) is a hint only."""
mono = _mono(y)
dur = mono.size / sr
one_shot = dur < THRESH["one_shot_s"]
kind = "one_shot" if one_shot else "loop"
s_dc, dc = dc_score(mono)
s_seam, seam_db = seam_score(mono)
s_zc, d0, d1 = zc_score(mono, sr)
s_lvl, rms_db, peak_db, clipped = level_score(mono)
s_bass, corr = bass_mono_score(y)
s_bar, bar = bar_consistency_score(mono, sr) if not one_shot else (1.0, {})
sub = {"seam": s_seam, "bar": s_bar, "zc": s_zc,
"level": s_lvl, "dc": s_dc, "bass_mono": s_bass}
w = WEIGHTS[kind]
grade = sum(w[k] * sub[k] for k in w)
flags: list[str] = []
if one_shot:
flags.append("one-shot")
if abs(dc) > THRESH["dc_ok"]:
flags.append("dc-offset")
if s_seam < 0.4 and not one_shot:
flags.append("seam-click")
if not one_shot and bar.get("best_bars", 0) == 0:
flags.append("off-grid")
if clipped:
flags.append("clipping")
if rms_db < -45:
flags.append("near-silent")
if (role == "bass" or y.ndim > 1) and corr < 0.2:
flags.append("mono-incompatible")
metrics = {
"dur_s": round(dur, 3), "sr": sr,
"channels": 1 if y.ndim == 1 else y.shape[0],
"rms_dbfs": round(rms_db, 2), "peak_dbfs": round(peak_db, 2),
"dc": round(dc, 6), "seam_click_db": round(seam_db, 2),
"zc_start_ms": round(d0, 2), "zc_end_ms": round(d1, 2),
"interchannel_corr": round(corr, 4), **bar,
}
g = LoopGrade(path=path, kind=kind, grade=round(grade, 4), tier="?",
sub={k: round(v, 4) for k, v in sub.items()},
metrics=metrics, flags=flags)
g.tier = g.tier_of()
return g
def grade(path: str | Path, *, role: Optional[str] = None) -> LoopGrade:
"""Grade a file on disk. `role` hint ('bass', 'drums', …) tunes a couple checks."""
y, sr = load_audio(path)
return grade_array(y, sr, path=str(path), role=role)
# ── CLI ───────────────────────────────────────────────────────────────────────
def main(argv: Optional[list[str]] = None) -> int:
import argparse
import json
ap = argparse.ArgumentParser(description="grade audio files as loops (the katana)")
ap.add_argument("wavs", nargs="+", help="audio file(s) to grade")
ap.add_argument("--role", default=None, help="role hint: bass|drums|other|vocals")
ap.add_argument("--json", action="store_true", help="emit JSON array")
a = ap.parse_args(argv)
grades = []
for w in a.wavs:
try:
grades.append(grade(w, role=a.role))
except Exception as e: # one bad file shouldn't sink a batch
print(f"✗ {w}: {e}")
if a.json:
print(json.dumps([g.model_dump() for g in grades], indent=2))
else:
for g in grades:
fl = f" ⚑ {', '.join(g.flags)}" if g.flags else ""
print(f"[{g.tier}] {g.grade:.3f} {g.kind:8} {Path(g.path).name}{fl}")
print(f" {' '.join(f'{k}={v:.2f}' for k, v in g.sub.items())}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
"""Tests for the loop grader (engine/grade.py, the katana).
Assertions are over SYNTHETIC signals with known properties, and are RELATIVE
(a perfect loop seams better than a torn one) rather than pinned to the v1
thresholds — those get calibrated against the corpus in tasks #11/#7, so pinning
them here would just freeze provisional numbers.
cd tools/foundry && python3 -m pytest tests/test_grade.py -q
"""
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from engine import grade as G
def _sine(freq=100.0, sr=44100, periods=200, dc=0.0, amp=0.3):
"""A sine of an INTEGER number of periods → wraps seamlessly."""
n = int(round(sr * periods / freq))
t = np.arange(n) / sr
return amp * np.sin(2 * np.pi * freq * t) + dc, sr
def test_one_shot_classified():
y, sr = _sine(periods=20) # ~0.2 s ≪ 0.75 s
g = G.grade_array(y, sr)
assert g.kind == "one_shot" and "one-shot" in g.flags
def test_loop_vs_oneshot_threshold():
short, sr = _sine(periods=60) # 0.6 s
long, _ = _sine(periods=120) # 1.2 s
assert G.grade_array(short, sr).kind == "one_shot"
assert G.grade_array(long, sr).kind == "loop"
def test_perfect_loop_seams_better_than_torn():
perfect, sr = _sine(periods=200) # integer periods → tiny wrap step
torn = perfect[: len(perfect) // 2 - 113] # cut mid-period → end ≠ start
s_perfect, _ = G.seam_score(perfect)
s_torn, _ = G.seam_score(torn)
assert s_perfect > s_torn
assert s_perfect > 0.7 # a clean wrap should grade high
def test_dc_offset_detected_and_flagged():
clean, sr = _sine(dc=0.0)
dirty, _ = _sine(dc=0.05)
s_clean, m_clean = G.dc_score(clean)
s_dirty, m_dirty = G.dc_score(dirty)
assert abs(m_dirty) > abs(m_clean)
assert s_dirty < s_clean
assert "dc-offset" in G.grade_array(dirty, sr).flags
def test_clipping_flagged():
y, sr = _sine(amp=2.0) # amp 2.0 → clips when read as peak ~0 dBFS
y = np.clip(y, -0.9999, 0.9999)
score, rms_db, peak_db, clipped = G.level_score(y)
assert clipped and peak_db > -1.0
assert "clipping" in G.grade_array(y, sr).flags
def test_near_silent_flagged():
y, sr = _sine(amp=0.0008) # ~ -62 dBFS
assert "near-silent" in G.grade_array(y, sr).flags
def test_mono_compatible_vs_anti_phase():
mono, sr = _sine(periods=200)
in_phase = np.stack([mono, mono]) # L == R → mono-sums fine
anti = np.stack([mono, -mono]) # L == -R → cancels on mono-sum
s_in, c_in = G.bass_mono_score(in_phase)
s_anti, c_anti = G.bass_mono_score(anti)
assert s_in > 0.9 and c_in > 0.9
assert s_anti < 0.1 and c_anti < -0.9
assert "mono-incompatible" in G.grade_array(anti, sr).flags
def test_mono_file_is_na_not_penalized():
y, sr = _sine(periods=200) # 1-D → mono
score, corr = G.bass_mono_score(y)
assert score == 1.0 # N/A, full marks
def _click_train(bpm=120.0, sr=44100, beats=8, decay=0.01):
"""Impulses at `bpm` quarter notes → a signal with a real, detectable tempo."""
spb = 60.0 / bpm # seconds per beat
n = int(round(sr * spb * beats))
y = np.zeros(n)
env_len = int(sr * decay)
env = np.exp(-np.arange(env_len) / (sr * decay / 5))
for b in range(beats):
i = int(round(b * spb * sr))
y[i:i + env_len] += env[: max(0, min(env_len, n - i))][: n - i]
return y, sr
def test_bar_consistency_finds_musical_interpretation():
# 8 quarter-note clicks @ 120 bpm = 4.0 s = 2 bars; tempo is genuinely detectable.
y, sr = _click_train(bpm=120.0, beats=8)
score, info = G.bar_consistency_score(y, sr)
assert info["best_bars"] in G.THRESH["bars"]
assert score > 0.3 # a clean 2-bar @ 120 grid should grade well
def test_off_grid_duration_scores_zero():
# 3.33 s with no {1,2,4,8}-bar interpretation in [60,200] bpm.
sr = 44100
n = int(sr * 3.33)
y = 0.3 * np.sin(2 * np.pi * 110 * np.arange(n) / sr)
# force the no-tempo path by checking the math: bars*4*60/3.33 for 1,2,4,8
implied = [b * 4 * 60 / 3.33 for b in (1, 2, 4, 8)]
in_range = [v for v in implied if 60 <= v <= 200]
if not in_range: # only assert when truly off-grid
score, info = G.bar_consistency_score(y, sr)
assert info["best_bars"] == 0 and score == 0.0
def test_grade_shape_and_tier_monotonic():
y, sr = _sine(periods=200)
g = G.grade_array(y, sr)
assert g.tier in {"S", "A", "B", "C", "D"}
assert 0.0 <= g.grade <= 1.0
assert set(g.sub) == {"seam", "bar", "zc", "level", "dc", "bass_mono"}
assert g.model_dump()["path"] == "<array>" # serializes for the tierlist
def test_tier_of_boundaries():
y, sr = _sine(periods=200)
g = G.grade_array(y, sr)
g.grade = 0.95; assert g.tier_of() == "S"
g.grade = 0.85; assert g.tier_of() == "A"
g.grade = 0.72; assert g.tier_of() == "B"
g.grade = 0.60; assert g.tier_of() == "C"
g.grade = 0.10; assert g.tier_of() == "D"
def test_load_audio_roundtrip(tmp_path):
import soundfile as sf
y, sr = _sine(periods=200)
p = tmp_path / "loop.wav"
sf.write(str(p), y.astype(np.float32), sr)
loaded, lsr = G.load_audio(p)
assert lsr == sr
assert loaded.ndim == 1 or loaded.shape[0] == 1 # mono written, mono read
g = G.grade(p)
assert g.path == str(p) and g.kind == "loop"
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