/** * Turning edited clips into a snippet library. Each included onset is * refined to the foot of its energy rise (the project's one definition of * event time) and a fixed window [t - pre, t + post) is cut. Onsets whose * window would run off either end of the clip are skipped rather than * padded, so every snippet is honest audio. */ import { refineOnset } from '../analysis/onsets.ts'; import type { EventType } from '../store.ts'; import { FS, type SnippetLibrary } from './types.ts'; export interface ClipAudioLookup { (clipId: string): { samples: Float32Array; sr: number } | null; } export function extractLibrary( types: EventType[], preMs: number, postMs: number, getAudio: ClipAudioLookup, ): SnippetLibrary { const pre = Math.round((preMs / 1000) * FS); const post = Math.round((postMs / 1000) * FS); const lib: SnippetLibrary = { names: types.map((t) => t.name), pre, post, snippets: [] }; types.forEach((type, typeIndex) => { for (const clip of type.clips) { const audio = getAudio(clip.id); if (!audio) continue; const excluded = new Set(clip.excluded); clip.onsets.forEach((t, i) => { if (excluded.has(i)) return; const s = refineOnset(audio.samples, audio.sr, t); if (s - pre < 0 || s + post > audio.samples.length) return; lib.snippets.push({ typeIndex, samples: audio.samples.slice(s - pre, s + post), }); }); } }); return lib; } /** Usable snippet count per type id, for the UI. */ export function snippetCounts( types: EventType[], preMs: number, postMs: number, getAudio: ClipAudioLookup, ): Map { const lib = extractLibrary(types, preMs, postMs, getAudio); const counts = new Map(); types.forEach((t, i) => { counts.set(t.id, lib.snippets.filter((s) => s.typeIndex === i).length); }); return counts; }