mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-23 05:11:34 +00:00
97 lines
4.0 KiB
JavaScript
97 lines
4.0 KiB
JavaScript
/*
|
||
* fee[dB]ack v0.3.0 — shared tuner DSP/math.
|
||
*
|
||
* A small, dependency-free YIN pitch detector + frequency→note/cents helper,
|
||
* used by the topbar tuner badge (and reusable by anything else that needs a
|
||
* lightweight readout). This intentionally does NOT reach into the external
|
||
* note_detect plugin's internals — it's a standalone copy of the standard
|
||
* algorithm (constitution P-II, plugin isolation). Browser-global as
|
||
* `window.tunerCore`; also CommonJS-exported for node tests (tests/js).
|
||
*/
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||
|
||
/**
|
||
* YIN pitch detection on a Float32 time-domain buffer.
|
||
* @returns {{frequency:number, confidence:number}} frequency in Hz (0 if
|
||
* none found), confidence in 0..1 (1 - aperiodicity).
|
||
*/
|
||
function yinDetect(buffer, sampleRate, threshold) {
|
||
threshold = threshold == null ? 0.12 : threshold;
|
||
const n = buffer.length;
|
||
const halfN = Math.floor(n / 2);
|
||
const yin = new Float32Array(halfN);
|
||
|
||
// Difference function.
|
||
for (let tau = 0; tau < halfN; tau++) {
|
||
let sum = 0;
|
||
for (let i = 0; i < halfN; i++) {
|
||
const delta = buffer[i] - buffer[i + tau];
|
||
sum += delta * delta;
|
||
}
|
||
yin[tau] = sum;
|
||
}
|
||
|
||
// Cumulative mean normalized difference.
|
||
yin[0] = 1;
|
||
let running = 0;
|
||
for (let tau = 1; tau < halfN; tau++) {
|
||
running += yin[tau];
|
||
yin[tau] = running > 0 ? yin[tau] * tau / running : 1;
|
||
}
|
||
|
||
// Absolute threshold: first dip below `threshold`, then the local min.
|
||
let tauEstimate = -1;
|
||
for (let tau = 2; tau < halfN; tau++) {
|
||
if (yin[tau] < threshold) {
|
||
while (tau + 1 < halfN && yin[tau + 1] < yin[tau]) tau++;
|
||
tauEstimate = tau;
|
||
break;
|
||
}
|
||
}
|
||
if (tauEstimate === -1) return { frequency: 0, confidence: 0 };
|
||
|
||
// Parabolic interpolation around the dip for sub-sample accuracy.
|
||
const x0 = tauEstimate > 0 ? tauEstimate - 1 : tauEstimate;
|
||
const x2 = tauEstimate + 1 < halfN ? tauEstimate + 1 : tauEstimate;
|
||
let betterTau = tauEstimate;
|
||
if (x0 !== tauEstimate && x2 !== tauEstimate) {
|
||
const s0 = yin[x0], s1 = yin[tauEstimate], s2 = yin[x2];
|
||
const denom = 2 * (2 * s1 - s2 - s0);
|
||
if (denom !== 0) betterTau = tauEstimate + (s2 - s0) / denom;
|
||
}
|
||
// A near-zero denom can fling betterTau out of [x0, x2] or non-finite,
|
||
// making frequency Infinity/NaN; clamp it back to the dip neighbourhood
|
||
// and treat a non-positive tau as "no detection" (frequency 0).
|
||
if (!Number.isFinite(betterTau) || betterTau < x0 || betterTau > x2) betterTau = tauEstimate;
|
||
return {
|
||
frequency: betterTau > 0 ? sampleRate / betterTau : 0,
|
||
confidence: Math.max(0, Math.min(1, 1 - yin[tauEstimate])),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Map a frequency to the nearest note + cents deviation.
|
||
* @param {number} freq Hz
|
||
* @param {number} [referencePitch=440] A4 reference (430–450)
|
||
* @returns {{name,octave,note,midi,cents,targetFreq}|null}
|
||
*/
|
||
function freqToNote(freq, referencePitch) {
|
||
if (!freq || freq <= 0) return null;
|
||
const a4 = referencePitch || 440;
|
||
const midiFloat = 69 + 12 * Math.log2(freq / a4);
|
||
const midi = Math.round(midiFloat);
|
||
const targetFreq = a4 * Math.pow(2, (midi - 69) / 12);
|
||
const cents = Math.round(1200 * Math.log2(freq / targetFreq));
|
||
const name = NOTE_NAMES[((midi % 12) + 12) % 12];
|
||
const octave = Math.floor(midi / 12) - 1;
|
||
return { name, octave, note: name + octave, midi, cents, targetFreq };
|
||
}
|
||
|
||
const api = { yinDetect, freqToNote, NOTE_NAMES };
|
||
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||
if (root) root.tunerCore = api;
|
||
})(typeof window !== 'undefined' ? window : null);
|