Commit 1bb7c4fa by PLN (Algolia)

fix(foundry): pocket-zc rotation junction + DC-free loop exports

Two refinements from measuring the first rotated re-cuts of the real kits:

1. Junction in the pre-attack pocket. The rotation point was zc-snapped
   ±symmetrically around the downbeat, which could land the loop wrap
   mid-transient (rotated hammer drums read seam 0.50 / click_db −0.03:
   physically continuous, but the junction sat in busy material and a
   re-trigger clipped into the groove). _pocket_zc() searches
   [−25 ms, +2 ms] around the target and picks the zero crossing with
   the smallest local 2 ms RMS — the natural cut point in the quiet dip
   before the hit. Re-trigger catches the full attack; hammer drums
   grade recovered A 0.808 → S 0.988, bass 0.861 → S 0.916.

2. Exports ship DC-free (B2), mirroring vox._finish_clip (4a74213c):
   the demucs stems' ~1e-4 DC bias was flowing into every exported loop
   (all three OLD superfreak kit files flag dc-offset on re-grade
   today). Mean-subtract after mono-sum, before write; shifts the
   zc-snapped edges by ≤1e-4 — inaudible, still effectively zero.

86 tests green.
parent 3f9c27df
...@@ -527,11 +527,38 @@ def _rotate_to_downbeat(clip: np.ndarray, sr: int, n_beats: int, ...@@ -527,11 +527,38 @@ def _rotate_to_downbeat(clip: np.ndarray, sr: int, n_beats: int,
best_k = int(force_slot) % n_beats best_k = int(force_slot) % n_beats
if best_k == 0: if best_k == 0:
return clip, 0, 0 return clip, 0, 0
r = _snap_zc(mono, int(round(best_k * n / n_beats)), sr) r = _pocket_zc(mono, int(round(best_k * n / n_beats)), sr)
rotated = np.roll(c2, -r, axis=0) rotated = np.roll(c2, -r, axis=0)
return (rotated if clip.ndim == 2 else rotated[:, 0]), best_k, r return (rotated if clip.ndim == 2 else rotated[:, 0]), best_k, r
def _pocket_zc(mono: np.ndarray, idx: int, sr: int, *,
back_s: float = 0.025, fwd_s: float = 0.002) -> int:
"""Zero crossing in the QUIET POCKET just before an attack at `idx` (circular).
A rotation junction placed exactly on the downbeat can land mid-transient (the plain
nearest-zc snap is ±symmetric); the natural cut point is the low-energy dip that
precedes the hit. Search [idx−back_s, idx+fwd_s] for zero crossings and pick the one
with the smallest local (2 ms) RMS — the junction then sits in the pre-kick pocket
and a re-trigger catches the full attack. Falls back to `idx` if no crossing.
"""
n = mono.size
lo, hi = idx - int(back_s * sr), idx + int(fwd_s * sr)
seg = mono[np.arange(lo, hi) % n]
zc = np.where(np.diff(np.signbit(seg)))[0]
if not zc.size:
return idx % n
w = max(4, int(0.002 * sr))
best, best_rms = None, np.inf
for z in zc:
pos = (lo + int(z)) % n
local = mono[np.arange(pos - w // 2, pos + w // 2) % n]
r = float(np.sqrt(np.mean(local ** 2)))
if r < best_rms:
best_rms, best = r, pos
return int(best)
def kit_multiples_check(files: list[Path], bpm: float, *, tol_pct: float = 0.5) -> dict: def kit_multiples_check(files: list[Path], bpm: float, *, tol_pct: float = 0.5) -> dict:
"""Per-file duration-in-bars at the shared grid `bpm`; flag any off an integer multiple. """Per-file duration-in-bars at the shared grid `bpm`; flag any off an integer multiple.
...@@ -630,6 +657,7 @@ def export_take(take: Take, kit: str, workspace: Path, catch: Catch, *, ...@@ -630,6 +657,7 @@ def export_take(take: Take, kit: str, workspace: Path, catch: Catch, *,
clip, _slot, _roll = _rotate_to_downbeat(clip, sr, n_beats, force_slot=slot) clip, _slot, _roll = _rotate_to_downbeat(clip, sr, n_beats, force_slot=slot)
if sl.name == "bass" and clip.shape[1] > 1: # B6 mono-sum if sl.name == "bass" and clip.shape[1] > 1: # B6 mono-sum
clip = clip.mean(axis=1, keepdims=True) clip = clip.mean(axis=1, keepdims=True)
clip = clip - clip.mean(axis=0, keepdims=True) # B2: ship DC-free
if peak_norm: # B10: off by default if peak_norm: # B10: off by default
pk = np.max(np.abs(clip)) + 1e-9 pk = np.max(np.abs(clip)) + 1e-9
clip = clip * (10 ** (-1.0 / 20) / pk) clip = clip * (10 ** (-1.0 / 20) / pk)
......
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