fix(align): calibrated fit (precision, lift, beat-shift check, baseline) and aligner guards (A7, A8)
Fit flags on precision (<0.6) and chance-corrected lift (<0.25), null with <8 recording onsets; old match rate kept as audibleRate (info). Symbolic-chroma beat/bar-shift check per window. Constant-offset baseline (-2..+2 s) replaces DTW without manual anchors unless DTW precision is better by >=0.02. Stretch warning counts in the fit summary. DTW: slope-limited steps, open begin/end options; per-bar outlier rejection (1/4 beat, Theil-Sen-transported +-3 bar median); no anchor in the final partial bar. bench real: saved/computed source, baseline + method, corrupt alignment.json skipped. Parabolic onset interpolation; warp extrapolation documented; spec amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3b002059e9
commit
3db87c0317
+17
-8
@@ -108,7 +108,11 @@ export function features(audio: Audio, rate: number): Features {
|
||||
return { rate, onset, chroma };
|
||||
}
|
||||
|
||||
/** Peak-pick an onset-strength curve (adaptive threshold median + k·MAD, 50 ms minimum gap). Times in seconds. */
|
||||
/**
|
||||
* Peak-pick an onset-strength curve (adaptive threshold median + k·MAD, 50 ms minimum gap, the stronger peak wins).
|
||||
* Peak times are refined to sub-frame precision by parabolic interpolation over the peak and its two neighbours.
|
||||
* Times in seconds.
|
||||
*/
|
||||
export function pickOnsets(onset: Float32Array, rate: number, k = 4): number[] {
|
||||
const n = onset.length;
|
||||
if (n === 0) return [];
|
||||
@@ -118,15 +122,20 @@ export function pickOnsets(onset: Float32Array, rate: number, k = 4): number[] {
|
||||
const mad = dev[n >> 1]! * 1.4826;
|
||||
const thr = med + k * Math.max(mad, 1e-3);
|
||||
const minGap = 0.05;
|
||||
const picked: number[] = [];
|
||||
const frames: number[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const v = onset[i]!;
|
||||
if (v <= thr || v < (onset[i - 1] ?? -Infinity) || v <= (onset[i + 1] ?? -Infinity)) continue;
|
||||
const t = i / rate;
|
||||
const last = picked.length - 1;
|
||||
if (last >= 0 && t - picked[last]! < minGap - 1e-9) {
|
||||
if (v > onset[Math.round(picked[last]! * rate)]!) picked[last] = t;
|
||||
} else picked.push(t);
|
||||
const last = frames.length - 1;
|
||||
if (last >= 0 && (i - frames[last]!) / rate < minGap - 1e-9) {
|
||||
if (v > onset[frames[last]!]!) frames[last] = i;
|
||||
} else frames.push(i);
|
||||
}
|
||||
return picked;
|
||||
return frames.map((i) => {
|
||||
if (i === 0 || i === n - 1) return i / rate;
|
||||
const a = onset[i - 1]!, b = onset[i]!, c = onset[i + 1]!;
|
||||
const den = a - 2 * b + c;
|
||||
const d = den < 0 ? Math.max(-0.5, Math.min(0.5, (0.5 * (a - c)) / den)) : 0;
|
||||
return (i + d) / rate;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user