1/**
2 * End-to-end pipeline test without a microphone: three synthetic event
3 * types (a low ringing tone, a high short tone, a noise burst) stand in
4 * for recorded taps. Builds a snippet library, synthesizes datasets across
5 * runs, executes each baseline algorithm through numbl, and prints scores.
6 * The matched filter and the kNN sorter should sit far above the naive
7 * floor; the run fails loudly if they do not.
8 */
9import { FS, type SynthConfig } from '../src/bench/types.ts';
10import { synthesize } from '../src/bench/synthesize.ts';
11import { score } from '../src/bench/score.ts';
12import { buildTraining } from '../src/bench/training.ts';
13import { runAlgorithm } from '../src/bench/execute.ts';
14import { makeSyntheticLibrary } from './synthetic-lib.ts';
15import matchedFilterSrc from '../src/algorithms/template_matched_filter.m?raw';
16import onsetKnnSrc from '../src/algorithms/onset_knn.m?raw';
17import silenceGapKnnSrc from '../src/algorithms/silence_gap_knn.m?raw';
18import naiveSrc from '../src/algorithms/naive_threshold.m?raw';
20const lib = makeSyntheticLibrary();
22const cfg: SynthConfig = {
23 durationS: 30,
24 eventsPerType: 30,
25 gainMinDb: -12,
26 overlap: 0.2,
27 trainFraction: 0.3,
28 seed: 1,
29};
31const algos = [
32 { name: 'template_matched_filter', source: matchedFilterSrc },
33 { name: 'onset_knn', source: onsetKnnSrc },
34 { name: 'silence_gap_knn', source: silenceGapKnnSrc },
35 { name: 'naive_threshold', source: naiveSrc },
36];
38const NRUNS = 2;
39const TOL = 0.02;
40const acc: Record<string, number[]> = {};
41for (let r = 0; r < NRUNS; r++) {
42 const ds = synthesize(lib, cfg, r);
43 const training = buildTraining(lib, ds.trainIndices);
44 console.log(`run ${r}: ${ds.truth.length} ground-truth events, ${ds.x.length} samples`);
45 for (const a of algos) {
46 const t0 = Date.now();
47 try {
48 const out = runAlgorithm(a.source, ds.x, FS, training);
49 const s = score(ds.truth, out.detections, lib.names.length, TOL);
50 (acc[a.name] ??= []).push(s.accuracy);
51 console.log(
52 ` ${a.name.padEnd(26)} acc=${s.accuracy.toFixed(3)} correct=${s.correct}` +
53 ` miss=${s.missed} fp=${s.falsePositives} misclass=${s.misclassified}` +
54 ` (${Date.now() - t0} ms)`,
55 );
56 } catch (err) {
57 console.log(` ${a.name.padEnd(26)} ERROR: ${(err as Error).message}`);
58 }
59 }
60}
62const mean = (v: number[]) => v.reduce((a, b) => a + b, 0) / Math.max(v.length, 1);
63console.log('\nmean accuracy over runs:');
64for (const a of algos) console.log(` ${a.name.padEnd(26)} ${mean(acc[a.name] ?? []).toFixed(3)}`);
66const mf = mean(acc['template_matched_filter'] ?? []);
67const knn = mean(acc['onset_knn'] ?? []);
68if (!(mf > 0.7)) { console.error('FAIL: matched filter accuracy too low'); process.exit(1); }
69if (!(knn > 0.5)) { console.error('FAIL: onset_knn accuracy too low'); process.exit(1); }
70console.log('PASS');