Commit 0c140b17 by PLN (Algolia)

fix(foundry): the periodicity check was measuring loop length, then role

Two errors stacked in one metric, both found by looking at its distribution instead
of its verdicts.

**It penalised short loops for being short.** Autocorrelation at lag k has only N-k
overlapping frames, but the score was normalised by ac[0], which sums all N — so the
number was partly a function of how much of the loop the lag consumed, and a 1-bar lag
consumes half a 2-bar loop against an eighth of an 8-bar one. 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 per-lag overlap count first: 0.834 / 0.830 / 0.862,
flat as it must be. Corpus medians moved 0.12 -> 0.24 (2 bar), 0.31 -> 0.41, 0.43 -> 0.49.

**And then it asked every role the same question.** Split by family across 81 bar-loops:
drums 0.754, fx 0.549, tonal 0.214, bass 0.173, vox 0.087 — an 8.7x spread end to end.
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, and a held chord is a perfectly good loop with no onset
period at all. One threshold across all of them condemned 12 of 20 vocal loops for the
crime of being vocal. Floors are now per family, and vox is measured-but-never-flagged.

Also: dead bars are now rejected at EXPORT rather than reported afterwards. A 4-bar loop
whose last two bars are a fade grades S — silent-to-silent is a perfect seam and the
length is still an exact bar multiple — and 10 of the first 161 cuts were exactly that.
An objective defect with an objective test belongs in the gate, not in a report someone
has to read.

First full run for the record: 161 samples, 43 kits, ZERO bar-length errors and 161 of
161 distinct sounds, so the bar-multiple guarantee and the export dedup both hold at
scale. 21 tests in this file, 104 in the suite.
parent ee41a29b
......@@ -42,6 +42,23 @@ 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".
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:
......@@ -72,18 +89,28 @@ def bar_levels(y: np.ndarray, sr: int, bars: int) -> list[float]:
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 half the loop, whichever the loop is long enough to have). A rhythmic loop
peaks there; a through-composed window does not.
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 period to check
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
......@@ -113,6 +140,11 @@ def _fingerprint(y: np.ndarray, sr: int, n: int = 256) -> np.ndarray:
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] = []
......@@ -147,9 +179,11 @@ def check_cuts(cuts: list[dict]) -> list[KitCheck]:
r.problems.append(f"dead bar(s) {dead}: {hi - lo:.0f} dB spread across bars")
r.periodicity = periodicity(m, sr, bars)
if r.periodicity < 0.15:
r.problems.append(f"weak bar periodicity ({r.periodicity:.2f}) — "
"bar-aligned but maybe not a repeating unit")
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)
......
......@@ -171,6 +171,8 @@ def _rank_by_grade(by_mode: dict[str, list], y_st: np.ndarray, family: str,
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.
......@@ -212,6 +214,20 @@ def track_tempo(grid) -> tuple[float, float]:
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.
......
......@@ -193,3 +193,34 @@ def test_a_fallback_guess_loses_to_the_measurement():
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)
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