Clean release snapshot

This commit is contained in:
byrongamatos
2026-06-16 18:47:13 +02:00
commit 6c110398b4
574 changed files with 162566 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
/*
* 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 (430450)
* @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);