/** * Whole-project export and import as one JSON file: event types with * their clip audio (base64), algorithm sources, and the benchmark * configuration. Benchmark results are deliberately left out; they are * reproducible from the config and the seed. */ import type { AlgorithmDef } from './bench/types.ts'; import type { BenchConfig, ClipMeta, EventType } from './store.ts'; export interface ProjectFile { format: 'audio-spike-sorting'; version: 1; types: (EventType & { clipAudio: Record })[]; config: BenchConfig; algorithms: AlgorithmDef[]; } function f32ToB64(x: Float32Array): string { const bytes = new Uint8Array(x.buffer, x.byteOffset, x.byteLength); let s = ''; const CHUNK = 0x8000; for (let i = 0; i < bytes.length; i += CHUNK) { s += String.fromCharCode(...bytes.subarray(i, i + CHUNK)); } return btoa(s); } function b64ToF32(b64: string): Float32Array { const bin = atob(b64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return new Float32Array(bytes.buffer); } export function exportProject( types: EventType[], config: BenchConfig, algorithms: AlgorithmDef[], getAudio: (clipId: string) => { samples: Float32Array; sr: number } | null, ): string { const doc: ProjectFile = { format: 'audio-spike-sorting', version: 1, types: types.map((t) => { const clipAudio: Record = {}; for (const c of t.clips) { const a = getAudio(c.id); if (a) clipAudio[c.id] = { b64: f32ToB64(a.samples), sr: a.sr }; } return { ...t, clipAudio }; }), config, algorithms, }; return JSON.stringify(doc); } export interface ImportedProject { types: EventType[]; config: BenchConfig; algorithms: AlgorithmDef[]; clipAudio: { id: string; samples: Float32Array; sr: number }[]; } export function importProject(json: string): ImportedProject { const doc = JSON.parse(json) as ProjectFile; if (doc.format !== 'audio-spike-sorting') throw new Error('not an audio-spike-sorting project file'); const clipAudio: ImportedProject['clipAudio'] = []; const types: EventType[] = doc.types.map((t) => { for (const [id, a] of Object.entries(t.clipAudio ?? {})) { clipAudio.push({ id, samples: b64ToF32(a.b64), sr: a.sr }); } const clips: ClipMeta[] = t.clips; return { id: t.id, name: t.name, color: t.color, clips }; }); return { types, config: doc.config, algorithms: doc.algorithms, clipAudio }; }