// Shared YIN pitch estimation over a PCM buffer. export function yinFrame(x, off, W, sr, fmin = 55, fmax = 400) { const tauMin = Math.floor(sr / fmax), tauMax = Math.min(Math.floor(sr / fmin), W - 1); if (off < 0 || off + W >= x.length || tauMax <= tauMin) return { f0: 0, conf: 0 }; const d = new Float32Array(tauMax + 1); const N = W - tauMax; for (let tau = tauMin; tau <= tauMax; tau += 1) { let s = 0; for (let i = 0; i < N; i += 1) { const dv = x[off + i] - x[off + i + tau]; s += dv * dv; } d[tau] = s; } const dp = new Float32Array(tauMax + 1); let run = 0; dp[0] = 1; for (let tau = tauMin; tau <= tauMax; tau += 1) { run += d[tau]; dp[tau] = run > 0 ? (d[tau] * (tau - tauMin + 1)) / run : 1; } let best = -1; for (let tau = tauMin + 1; tau < tauMax; tau += 1) if (dp[tau] < 0.15 && dp[tau] <= dp[tau + 1]) { best = tau; break; } if (best < 0) { let m = Infinity; for (let tau = tauMin; tau <= tauMax; tau += 1) if (dp[tau] < m) { m = dp[tau]; best = tau; } } if (best <= tauMin || best >= tauMax) return { f0: 0, conf: 0 }; const a = dp[best - 1], b = dp[best], c = dp[best + 1]; const den = a - 2 * b + c; const tau = best + (den !== 0 ? Math.max(-1, Math.min(1, (0.5 * (a - c)) / den)) : 0); return { f0: sr / tau, conf: 1 - dp[best] }; } // Median f0 over a region, weighted to confident frames. export function regionPitch(x, from, to, sr, fmin = 55, fmax = 400) { const W = Math.min(2048, Math.max(512, to - from)); const hop = Math.max(128, Math.round(sr * 0.01)); const vals = []; for (let off = from; off + W < to + W && off + W < x.length; off += hop) { const r = yinFrame(x, off, W, sr, fmin, fmax); if (r.conf >= 0.55 && r.f0 > 0) vals.push(r.f0); if (off > to) break; } if (!vals.length) return { f0: 0, n: 0 }; vals.sort((a, b) => a - b); return { f0: vals[vals.length >> 1], n: vals.length }; } export const midiOf = (hz) => 69 + 12 * Math.log2(hz / 440);