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
"""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