/** * YIN pitch detection worker. * * Receives { samples: Float32Array, sampleRate: number }, posts back * { freq, confidence, rms }. The samples ArrayBuffer should be passed * as transferable so no copy occurs across the worker boundary. */ // Guard allows the file to be required in Node.js test environments where // `self` is not defined; the worker message handler only runs in the browser. if (typeof self !== 'undefined') { self.onmessage = (e) => { const { samples, sampleRate } = e.data; self.postMessage(_yinDetect(samples, sampleRate)); }; } function _yinDetect(buffer, sampleRate) { const threshold = 0.15; const halfLen = Math.floor(buffer.length / 2); const yinBuffer = new Float32Array(halfLen); let rms = 0; for (let i = 0; i < buffer.length; i++) rms += buffer[i] * buffer[i]; rms = Math.sqrt(rms / buffer.length); if (rms < 0.01) return { freq: 0, confidence: 0, rms }; let runningSum = 0; yinBuffer[0] = 1; for (let tau = 1; tau < halfLen; tau++) { let sum = 0; for (let i = 0; i < halfLen; i++) { const delta = buffer[i] - buffer[i + tau]; sum += delta * delta; } yinBuffer[tau] = sum; runningSum += sum; yinBuffer[tau] = runningSum > 0 ? yinBuffer[tau] * tau / runningSum : 1; } // Canonical YIN absolute-threshold step: walk tau upward and take the FIRST // local minimum that drops below the threshold — NOT the globally deepest // dip. The fundamental period is the smallest tau that satisfies the // difference function; its sub-octaves (2T, 3T, …) sit at LARGER tau and // often dip just as deep or deeper (the waveform realigns over two full // periods), so picking the deepest dip is what produced the octave-low // errors — D1 reported for a D2 string, or the common sub-harmonic of a // two-note pluck. Choosing the first qualifying dip rejects those at the // source. We still track the global minimum as a fallback for signals where // nothing crosses the threshold. let tau = -1; let minVal = 1, minTau = -1; for (let t = 2; t < halfLen; t++) { if (yinBuffer[t] < minVal) { minVal = yinBuffer[t]; minTau = t; } if (yinBuffer[t] < threshold) { // Descend to the bottom of this first qualifying dip, keeping the // global-min tracker current for the indices we skip (otherwise the // fallback below could interpolate around a non-minimum). while (t + 1 < halfLen && yinBuffer[t + 1] < yinBuffer[t]) { t++; if (yinBuffer[t] < minVal) { minVal = yinBuffer[t]; minTau = t; } } tau = t; break; } } if (tau === -1) { // Nothing crossed the threshold — fall back to the global minimum so a // weak but periodic signal still yields an estimate (confidence will be // correspondingly low and is filtered downstream). if (minTau === -1) return { freq: 0, confidence: 0, rms }; tau = minTau; } const s0 = yinBuffer[tau - 1]; const s1 = yinBuffer[tau]; const s2 = tau + 1 < halfLen ? yinBuffer[tau + 1] : yinBuffer[tau]; const denom = s0 - 2 * s1 + s2; let betterTau = denom === 0 ? tau : tau + (s0 - s2) / (2 * denom); // A near-zero (but nonzero) denom can fling the parabolic estimate far // outside the bracket; the true minimum is within ±1 sample of tau. The // negated test also rejects NaN. if (!(betterTau >= tau - 1 && betterTau <= tau + 1)) betterTau = tau; return { freq: sampleRate / betterTau, confidence: 1 - yinBuffer[tau], rms }; } // Allow direct import in Node.js test environments; harmless in browser workers // where the `module` global is undefined. if (typeof module !== 'undefined') module.exports = { _yinDetect };