21abc12audio-spike-sorting: benchmark framework for audio event detection and classificationJeremy Magland 1/**
2 * Turning edited clips into a snippet library. Each included onset is
3 * refined to the foot of its energy rise (the project's one definition of
4 * event time) and a fixed window [t - pre, t + post) is cut. Onsets whose
5 * window would run off either end of the clip are skipped rather than
6 * padded, so every snippet is honest audio.
7 */
8import { refineOnset } from '../analysis/onsets.ts';
9import type { EventType } from '../store.ts';
10import { FS, type SnippetLibrary } from './types.ts';
12export interface ClipAudioLookup {
13 (clipId: string): { samples: Float32Array; sr: number } | null;
14}
16export function extractLibrary(
17 types: EventType[],
18 preMs: number,
19 postMs: number,
20 getAudio: ClipAudioLookup,
21): SnippetLibrary {
22 const pre = Math.round((preMs / 1000) * FS);
23 const post = Math.round((postMs / 1000) * FS);
24 const lib: SnippetLibrary = { names: types.map((t) => t.name), pre, post, snippets: [] };
25 types.forEach((type, typeIndex) => {
26 for (const clip of type.clips) {
27 const audio = getAudio(clip.id);
28 if (!audio) continue;
29 const excluded = new Set(clip.excluded);
30 clip.onsets.forEach((t, i) => {
31 if (excluded.has(i)) return;
32 const s = refineOnset(audio.samples, audio.sr, t);
33 if (s - pre < 0 || s + post > audio.samples.length) return;
34 lib.snippets.push({
35 typeIndex,
36 samples: audio.samples.slice(s - pre, s + post),
37 });
38 });
39 }
40 });
41 return lib;
42}
44/** Usable snippet count per type id, for the UI. */
45export function snippetCounts(
46 types: EventType[],
47 preMs: number,
48 postMs: number,
49 getAudio: ClipAudioLookup,
50): Map<string, number> {
51 const lib = extractLibrary(types, preMs, postMs, getAudio);
52 const counts = new Map<string, number>();
53 types.forEach((t, i) => {
54 counts.set(t.id, lib.snippets.filter((s) => s.typeIndex === i).length);
55 });
56 return counts;
57}