/ proof-of-concept / audio-spike-sorting
Sign in
proof-of-concept / audio-spike-sorting
audio-spike-sorting / scripts / test-node.ts
117 lines · 4.5 KBCodeBlameHistory
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 { mulberry32 } from '../src/bench/rng.ts';
10import { FS, type SnippetLibrary, type SynthConfig } from '../src/bench/types.ts';
11import { synthesize } from '../src/bench/synthesize.ts';
12import { score } from '../src/bench/score.ts';
13import { buildTraining } from '../src/bench/training.ts';
14import { runAlgorithm } from '../src/bench/execute.ts';
15import matchedFilterSrc from '../src/algorithms/template_matched_filter.m?raw';
16import onsetKnnSrc from '../src/algorithms/onset_knn.m?raw';
17import naiveSrc from '../src/algorithms/naive_threshold.m?raw';
19const PRE_S = 0.02, POST_S = 0.18;
20const pre = Math.round(PRE_S * FS), post = Math.round(POST_S * FS);
21const snipLen = pre + post;
23// A tap excites the struck object's impulse response, so instances of one
24// type are highly repeatable up to gain and small decay/pitch drift. The
25// burst type reuses one fixed noise pattern (the "object") rather than
26// fresh noise per instance.
27const burstBase = new Float32Array(post);
29 const brng = mulberry32(7);
30 for (let i = 0; i < post; i++) burstBase[i] = brng() * 2 - 1;
33function makeSnippet(kind: number, rng: () => number): Float32Array {
34 const s = new Float32Array(snipLen);
35 const jitF = 1 + 0.01 * (rng() - 0.5);
36 const jitT = 1 + 0.2 * (rng() - 0.5);
37 if (kind === 0) {
38 // low ringing tone ~300 Hz, slow decay
39 const f = 300 * jitF, tau = 0.06 * jitT;
40 for (let i = 0; i < post; i++) {
41 const t = i / FS;
42 s[pre + i] = Math.exp(-t / tau) * Math.sin(2 * Math.PI * f * t) * 0.8;
43 }
44 } else if (kind === 1) {
45 // high short tone ~1400 Hz, fast decay
46 const f = 1400 * jitF, tau = 0.025 * jitT;
47 for (let i = 0; i < post; i++) {
48 const t = i / FS;
49 s[pre + i] = Math.exp(-t / tau) * Math.sin(2 * Math.PI * f * t) * 0.8;
50 }
51 } else {
52 // repeatable noise burst, very fast decay
53 const tau = 0.012 * jitT;
54 for (let i = 0; i < post; i++) {
55 const t = i / FS;
56 s[pre + i] = Math.exp(-t / tau) * (burstBase[i] + 0.15 * (rng() * 2 - 1)) * 0.7;
57 }
58 }
59 // a touch of recording noise everywhere
60 for (let i = 0; i < snipLen; i++) s[i] += (rng() * 2 - 1) * 0.002;
61 return s;
64const rng = mulberry32(42);
65const lib: SnippetLibrary = { names: ['low', 'high', 'burst'], pre, post, snippets: [] };
66for (let k = 0; k < 3; k++) {
67 for (let e = 0; e < 120; e++) lib.snippets.push({ typeIndex: k, samples: makeSnippet(k, rng) });
70const cfg: SynthConfig = {
71 durationS: 30,
72 eventsPerType: 30,
73 gainMinDb: -12,
74 overlap: 0.2,
75 trainFraction: 0.3,
76 seed: 1,
77};
79const algos = [
80 { name: 'template_matched_filter', source: matchedFilterSrc },
81 { name: 'onset_knn', source: onsetKnnSrc },
82 { name: 'naive_threshold', source: naiveSrc },
83];
85const NRUNS = 2;
86const TOL = 0.02;
87const acc: Record<string, number[]> = {};
88for (let r = 0; r < NRUNS; r++) {
89 const ds = synthesize(lib, cfg, r);
90 const training = buildTraining(lib, ds.trainIndices);
91 console.log(`run ${r}: ${ds.truth.length} ground-truth events, ${ds.x.length} samples`);
92 for (const a of algos) {
93 const t0 = Date.now();
94 try {
95 const out = runAlgorithm(a.source, ds.x, FS, training);
96 const s = score(ds.truth, out.detections, lib.names.length, TOL);
97 (acc[a.name] ??= []).push(s.accuracy);
98 console.log(
99 ` ${a.name.padEnd(26)} acc=${s.accuracy.toFixed(3)} correct=${s.correct}` +
100 ` miss=${s.missed} fp=${s.falsePositives} misclass=${s.misclassified}` +
101 ` (${Date.now() - t0} ms)`,
102 );
103 } catch (err) {
104 console.log(` ${a.name.padEnd(26)} ERROR: ${(err as Error).message}`);
105 }
106 }
109const mean = (v: number[]) => v.reduce((a, b) => a + b, 0) / Math.max(v.length, 1);
110console.log('\nmean accuracy over runs:');
111for (const a of algos) console.log(` ${a.name.padEnd(26)} ${mean(acc[a.name] ?? []).toFixed(3)}`);
113const mf = mean(acc['template_matched_filter'] ?? []);
114const knn = mean(acc['onset_knn'] ?? []);
115if (!(mf > 0.7)) { console.error('FAIL: matched filter accuracy too low'); process.exit(1); }
116if (!(knn > 0.5)) { console.error('FAIL: onset_knn accuracy too low'); process.exit(1); }
117console.log('PASS');
↑↓ move↵openescclose