/** * Persistence. Small structured state lives in localStorage: event types * with their clips' onset lists, algorithm sources, and the benchmark * configuration. Bulk audio lives in IndexedDB: clip audio, and the last * benchmark's synthesized recordings so the explorer can reopen them * after a reload. The stores can drift only in harmless directions: a * clip whose audio is missing simply stops contributing snippets. */ import type { AlgorithmDef, RunResult, SynthConfig } from './bench/types.ts'; export interface ClipMeta { id: string; sens: number; // the sensitivity the onsets were detected with onsets: number[]; // seconds excluded: number[]; // indices into onsets } export interface EventType { id: string; name: string; color: string; clips: ClipMeta[]; } export interface BenchConfig extends SynthConfig { preMs: number; postMs: number; runs: number; toleranceMs: number; } export const DEFAULT_CONFIG: BenchConfig = { durationS: 30, eventsPerType: 30, gainMinDb: 0, overlap: 0.2, coincidence: 0, coincidenceMs: 5, trainFraction: 0.3, seed: 1, preMs: 20, postMs: 180, runs: 5, toleranceMs: 20, }; const KEY = 'audio-spike-sorting/v1'; interface Doc { types: EventType[]; config: BenchConfig; algorithms: AlgorithmDef[]; } export function loadDoc(): Doc { try { const raw = localStorage.getItem(KEY); if (raw) { const d = JSON.parse(raw) as Doc; d.config = { ...DEFAULT_CONFIG, ...d.config }; return d; } } catch { /* fall through */ } return { types: [], config: { ...DEFAULT_CONFIG }, algorithms: [] }; } export function saveDoc(doc: Doc): void { localStorage.setItem(KEY, JSON.stringify(doc)); } // ---- bulk data in IndexedDB ------------------------------------------------- const STORES = ['clips', 'bench'] as const; let dbPromise: Promise | null = null; function db(): Promise { return (dbPromise ??= new Promise((resolve, reject) => { const req = indexedDB.open('audio-spike-sorting', 1); req.onupgradeneeded = () => { for (const s of STORES) { if (!req.result.objectStoreNames.contains(s)) req.result.createObjectStore(s); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); })); } function op(store: string, mode: IDBTransactionMode, fn: (s: IDBObjectStore) => IDBRequest): Promise { return db().then((d) => new Promise((resolve, reject) => { const req = fn(d.transaction(store, mode).objectStore(store)); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); })); } export function putClipAudio(id: string, samples: Float32Array, sr: number): Promise { return op('clips', 'readwrite', (s) => s.put({ samples, sr }, id)); } export function getClipAudio(id: string): Promise<{ samples: Float32Array; sr: number } | null> { return op<{ samples: Float32Array; sr: number } | undefined>('clips', 'readonly', (s) => s.get(id)) .then((r) => r ?? null); } export function deleteClipAudio(id: string): Promise { return op('clips', 'readwrite', (s) => s.delete(id)); } /** The last completed benchmark, everything the explorer and the results * tables need to come back after a reload. */ export interface StoredBenchmark { when: number; names: string[]; colors: string[]; algos: { id: string; name: string }[]; config: BenchConfig; runs: { seed: number; x: Float32Array; truth: { t: number; typeIndex: number; gain: number }[]; /** Per type, concatenated training snippets; absent in benchmarks * stored before the dataset download existed. */ training?: { data: Float32Array; count: number }[]; }[]; results: RunResult[]; } export function putBenchmark(b: StoredBenchmark): Promise { return op('bench', 'readwrite', (s) => s.put(b, 'last')); } export function getBenchmark(): Promise { return op('bench', 'readonly', (s) => s.get('last')).then((r) => r ?? null); } export async function clearAll(): Promise { localStorage.removeItem(KEY); for (const s of STORES) await op(s, 'readwrite', (st) => st.clear()); }