Files
feedBack/tests/plugins/tuner/js/yin.unit.test.js
T
2026-06-16 18:47:13 +02:00

97 lines
3.9 KiB
JavaScript

'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { _yinDetect } = require('../../../../plugins/tuner/workers/yin.js');
const SAMPLE_RATE = 44100;
const FRAME = 4096;
function sine(freq, nSamples, amplitude = 0.8) {
const buf = new Float32Array(nSamples);
for (let i = 0; i < nSamples; i++) {
buf[i] = amplitude * Math.sin(2 * Math.PI * freq * i / SAMPLE_RATE);
}
return buf;
}
function assertFreq(result, expected, tolerancePct = 1.5, label = '') {
assert.ok(result.freq > 0, `${label}: expected a detected frequency, got 0`);
const errorPct = Math.abs(result.freq - expected) / expected * 100;
assert.ok(
errorPct < tolerancePct,
`${label}: expected ${expected} Hz ±${tolerancePct}%, got ${result.freq.toFixed(2)} Hz (error ${errorPct.toFixed(2)}%)`
);
}
// ── Silence / no-signal ──────────────────────────────────────────────────────
test('silence (all zeros) returns freq 0', () => {
const result = _yinDetect(new Float32Array(FRAME), SAMPLE_RATE);
assert.equal(result.freq, 0);
assert.equal(result.confidence, 0);
});
test('sub-threshold amplitude returns freq 0', () => {
// rms < 0.01 must be rejected before any pitch estimation
const buf = sine(440, FRAME, 0.001);
const result = _yinDetect(buf, SAMPLE_RATE);
assert.equal(result.freq, 0);
});
// ── Clean sine pitches — guitar range ────────────────────────────────────────
test('detects E2 (82.41 Hz) — low E string', () => {
assertFreq(_yinDetect(sine(82.41, FRAME), SAMPLE_RATE), 82.41, 1.5, 'E2');
});
test('detects A2 (110 Hz) — A string', () => {
assertFreq(_yinDetect(sine(110, FRAME), SAMPLE_RATE), 110, 1.5, 'A2');
});
test('detects D3 (146.83 Hz) — D string', () => {
assertFreq(_yinDetect(sine(146.83, FRAME), SAMPLE_RATE), 146.83, 1.5, 'D3');
});
test('detects G3 (196 Hz) — G string', () => {
assertFreq(_yinDetect(sine(196, FRAME), SAMPLE_RATE), 196, 1.5, 'G3');
});
test('detects B3 (246.94 Hz) — B string', () => {
assertFreq(_yinDetect(sine(246.94, FRAME), SAMPLE_RATE), 246.94, 1.5, 'B3');
});
test('detects E4 (329.63 Hz) — high E string', () => {
assertFreq(_yinDetect(sine(329.63, FRAME), SAMPLE_RATE), 329.63, 1.5, 'E4');
});
test('detects A4 (440 Hz) — concert A', () => {
assertFreq(_yinDetect(sine(440, FRAME), SAMPLE_RATE), 440, 1.5, 'A4');
});
// ── Confidence ────────────────────────────────────────────────────────────────
test('high confidence for clean sine at A4', () => {
const result = _yinDetect(sine(440, FRAME), SAMPLE_RATE);
assert.ok(result.confidence > 0.8, `Expected confidence > 0.8, got ${result.confidence.toFixed(3)}`);
});
test('rms reflects signal level', () => {
const quiet = _yinDetect(sine(440, FRAME, 0.1), SAMPLE_RATE);
const loud = _yinDetect(sine(440, FRAME, 0.9), SAMPLE_RATE);
assert.ok(loud.rms > quiet.rms, 'louder signal should have higher rms');
});
// ── Edge cases ────────────────────────────────────────────────────────────────
test('minimum buffer size (4096 samples) does not throw', () => {
assert.doesNotThrow(() => _yinDetect(sine(440, 4096), SAMPLE_RATE));
});
test('returns object with freq, confidence, rms keys', () => {
const result = _yinDetect(sine(440, FRAME), SAMPLE_RATE);
assert.ok('freq' in result, 'missing freq');
assert.ok('confidence' in result, 'missing confidence');
assert.ok('rms' in result, 'missing rms');
});