21abc12audio-spike-sorting: benchmark framework for audio event detection and classificationJeremy Magland 1/**
2 * Persistence. Small structured state lives in localStorage: event types
3 * with their clips' onset lists, algorithm sources, and the benchmark
4 * configuration. Bulk audio lives in IndexedDB: clip audio, and the last
5 * benchmark's synthesized recordings so the explorer can reopen them
6 * after a reload. The stores can drift only in harmless directions: a
7 * clip whose audio is missing simply stops contributing snippets.
8 */
9import type { AlgorithmDef, RunResult, SynthConfig } from './bench/types.ts';
11export interface ClipMeta {
12 id: string;
13 sens: number; // the sensitivity the onsets were detected with
14 onsets: number[]; // seconds
15 excluded: number[]; // indices into onsets
16}
18export interface EventType {
19 id: string;
20 name: string;
21 color: string;
22 clips: ClipMeta[];
23}
25export interface BenchConfig extends SynthConfig {
26 preMs: number;
27 postMs: number;
28 runs: number;
29 toleranceMs: number;
30}
32export const DEFAULT_CONFIG: BenchConfig = {
33 durationS: 30,
34 eventsPerType: 30,
e06c5e1Default to original event volumes; rename gain knob to level variation with an explanatory tooltipJeremy Magland 35 gainMinDb: 0,
21abc12audio-spike-sorting: benchmark framework for audio event detection and classificationJeremy Magland 36 overlap: 0.2,
df3f613Add a coincidence knob: a fraction of overlapping events land within a small window of their host's onset, the hardest case for detection and one the scorer's greedy matching handles with a documented caveatJeremy Magland 37 coincidence: 0,
38 coincidenceMs: 5,
21abc12audio-spike-sorting: benchmark framework for audio event detection and classificationJeremy Magland 39 trainFraction: 0.3,
40 seed: 1,
41 preMs: 20,
42 postMs: 180,
43 runs: 5,
44 toleranceMs: 20,
45};
47const KEY = 'audio-spike-sorting/v1';
49interface Doc {
50 types: EventType[];
51 config: BenchConfig;
52 algorithms: AlgorithmDef[];
53}
55export function loadDoc(): Doc {
56 try {
57 const raw = localStorage.getItem(KEY);
58 if (raw) {
59 const d = JSON.parse(raw) as Doc;
60 d.config = { ...DEFAULT_CONFIG, ...d.config };
61 return d;
62 }
63 } catch { /* fall through */ }
64 return { types: [], config: { ...DEFAULT_CONFIG }, algorithms: [] };
65}
67export function saveDoc(doc: Doc): void {
68 localStorage.setItem(KEY, JSON.stringify(doc));
69}
71// ---- bulk data in IndexedDB -------------------------------------------------
73const STORES = ['clips', 'bench'] as const;
75let dbPromise: Promise<IDBDatabase> | null = null;
76function db(): Promise<IDBDatabase> {
77 return (dbPromise ??= new Promise((resolve, reject) => {
78 const req = indexedDB.open('audio-spike-sorting', 1);
79 req.onupgradeneeded = () => {
80 for (const s of STORES) {
81 if (!req.result.objectStoreNames.contains(s)) req.result.createObjectStore(s);
82 }
83 };
84 req.onsuccess = () => resolve(req.result);
85 req.onerror = () => reject(req.error);
86 }));
87}
89function op<T>(store: string, mode: IDBTransactionMode, fn: (s: IDBObjectStore) => IDBRequest<T>): Promise<T> {
90 return db().then((d) => new Promise<T>((resolve, reject) => {
91 const req = fn(d.transaction(store, mode).objectStore(store));
92 req.onsuccess = () => resolve(req.result);
93 req.onerror = () => reject(req.error);
94 }));
95}
97export function putClipAudio(id: string, samples: Float32Array, sr: number): Promise<unknown> {
98 return op('clips', 'readwrite', (s) => s.put({ samples, sr }, id));
99}
100export function getClipAudio(id: string): Promise<{ samples: Float32Array; sr: number } | null> {
101 return op<{ samples: Float32Array; sr: number } | undefined>('clips', 'readonly', (s) => s.get(id))
102 .then((r) => r ?? null);
103}
104export function deleteClipAudio(id: string): Promise<unknown> {
105 return op('clips', 'readwrite', (s) => s.delete(id));
106}
108/** The last completed benchmark, everything the explorer and the results
109 * tables need to come back after a reload. */
110export interface StoredBenchmark {
111 when: number;
112 names: string[];
113 colors: string[];
114 algos: { id: string; name: string }[];
115 config: BenchConfig;
116 runs: {
117 seed: number;
118 x: Float32Array;
119 truth: { t: number; typeIndex: number; gain: number }[];
16328b9Download runs as a .zip for offline development: WAVs, ground truth, training snippets, algorithm sources, and a numbl evaluation harness that reproduces the browser scoresJeremy Magland 120 /** Per type, concatenated training snippets; absent in benchmarks
121 * stored before the dataset download existed. */
122 training?: { data: Float32Array; count: number }[];
21abc12audio-spike-sorting: benchmark framework for audio event detection and classificationJeremy Magland 123 }[];
124 results: RunResult[];
125}
127export function putBenchmark(b: StoredBenchmark): Promise<unknown> {
128 return op('bench', 'readwrite', (s) => s.put(b, 'last'));
129}
130export function getBenchmark(): Promise<StoredBenchmark | null> {
131 return op<StoredBenchmark | undefined>('bench', 'readonly', (s) => s.get('last')).then((r) => r ?? null);
132}
134export async function clearAll(): Promise<void> {
135 localStorage.removeItem(KEY);
136 for (const s of STORES) await op(s, 'readwrite', (st) => st.clear());
137}