/** * End-to-end pipeline test without a microphone: three synthetic event * types (a low ringing tone, a high short tone, a noise burst) stand in * for recorded taps. Builds a snippet library, synthesizes datasets across * runs, executes each baseline algorithm through numbl, and prints scores. * The matched filter and the kNN sorter should sit far above the naive * floor; the run fails loudly if they do not. */ import { mulberry32 } from '../src/bench/rng.ts'; import { FS, type SnippetLibrary, type SynthConfig } from '../src/bench/types.ts'; import { synthesize } from '../src/bench/synthesize.ts'; import { score } from '../src/bench/score.ts'; import { buildTraining } from '../src/bench/training.ts'; import { runAlgorithm } from '../src/bench/execute.ts'; import matchedFilterSrc from '../src/algorithms/template_matched_filter.m?raw'; import onsetKnnSrc from '../src/algorithms/onset_knn.m?raw'; import naiveSrc from '../src/algorithms/naive_threshold.m?raw'; const PRE_S = 0.02, POST_S = 0.18; const pre = Math.round(PRE_S * FS), post = Math.round(POST_S * FS); const snipLen = pre + post; // A tap excites the struck object's impulse response, so instances of one // type are highly repeatable up to gain and small decay/pitch drift. The // burst type reuses one fixed noise pattern (the "object") rather than // fresh noise per instance. const burstBase = new Float32Array(post); { const brng = mulberry32(7); for (let i = 0; i < post; i++) burstBase[i] = brng() * 2 - 1; } function makeSnippet(kind: number, rng: () => number): Float32Array { const s = new Float32Array(snipLen); const jitF = 1 + 0.01 * (rng() - 0.5); const jitT = 1 + 0.2 * (rng() - 0.5); if (kind === 0) { // low ringing tone ~300 Hz, slow decay const f = 300 * jitF, tau = 0.06 * jitT; for (let i = 0; i < post; i++) { const t = i / FS; s[pre + i] = Math.exp(-t / tau) * Math.sin(2 * Math.PI * f * t) * 0.8; } } else if (kind === 1) { // high short tone ~1400 Hz, fast decay const f = 1400 * jitF, tau = 0.025 * jitT; for (let i = 0; i < post; i++) { const t = i / FS; s[pre + i] = Math.exp(-t / tau) * Math.sin(2 * Math.PI * f * t) * 0.8; } } else { // repeatable noise burst, very fast decay const tau = 0.012 * jitT; for (let i = 0; i < post; i++) { const t = i / FS; s[pre + i] = Math.exp(-t / tau) * (burstBase[i] + 0.15 * (rng() * 2 - 1)) * 0.7; } } // a touch of recording noise everywhere for (let i = 0; i < snipLen; i++) s[i] += (rng() * 2 - 1) * 0.002; return s; } const rng = mulberry32(42); const lib: SnippetLibrary = { names: ['low', 'high', 'burst'], pre, post, snippets: [] }; for (let k = 0; k < 3; k++) { for (let e = 0; e < 120; e++) lib.snippets.push({ typeIndex: k, samples: makeSnippet(k, rng) }); } const cfg: SynthConfig = { durationS: 30, eventsPerType: 30, gainMinDb: -12, overlap: 0.2, trainFraction: 0.3, seed: 1, }; const algos = [ { name: 'template_matched_filter', source: matchedFilterSrc }, { name: 'onset_knn', source: onsetKnnSrc }, { name: 'naive_threshold', source: naiveSrc }, ]; const NRUNS = 2; const TOL = 0.02; const acc: Record = {}; for (let r = 0; r < NRUNS; r++) { const ds = synthesize(lib, cfg, r); const training = buildTraining(lib, ds.trainIndices); console.log(`run ${r}: ${ds.truth.length} ground-truth events, ${ds.x.length} samples`); for (const a of algos) { const t0 = Date.now(); try { const out = runAlgorithm(a.source, ds.x, FS, training); const s = score(ds.truth, out.detections, lib.names.length, TOL); (acc[a.name] ??= []).push(s.accuracy); console.log( ` ${a.name.padEnd(26)} acc=${s.accuracy.toFixed(3)} correct=${s.correct}` + ` miss=${s.missed} fp=${s.falsePositives} misclass=${s.misclassified}` + ` (${Date.now() - t0} ms)`, ); } catch (err) { console.log(` ${a.name.padEnd(26)} ERROR: ${(err as Error).message}`); } } } const mean = (v: number[]) => v.reduce((a, b) => a + b, 0) / Math.max(v.length, 1); console.log('\nmean accuracy over runs:'); for (const a of algos) console.log(` ${a.name.padEnd(26)} ${mean(acc[a.name] ?? []).toFixed(3)}`); const mf = mean(acc['template_matched_filter'] ?? []); const knn = mean(acc['onset_knn'] ?? []); if (!(mf > 0.7)) { console.error('FAIL: matched filter accuracy too low'); process.exit(1); } if (!(knn > 0.5)) { console.error('FAIL: onset_knn accuracy too low'); process.exit(1); } console.log('PASS');