mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-24 05:41:53 +00:00
90 lines
3.8 KiB
JavaScript
90 lines
3.8 KiB
JavaScript
/**
|
|
* 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 };
|