bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 1/**
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 2 * Persistence. Small structured state lives in localStorage: voices with
3 * their training-clip descriptions and cached features, per-instrument
4 * geometry overrides, the take's events, and UI knobs. Bulk audio and
5 * binary data live in IndexedDB: training-clip audio, the take's audio,
6 * and the bakes (the drum's is a couple of hundred MB, which is exactly
7 * why it is worth keeping). The stores can drift only in harmless
8 * directions: a clip whose audio is missing cannot be re-edited but keeps
9 * classifying, and a bake whose key no longer matches is ignored.
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 10 */
11import type { Binding } from './render/types.ts';
13export interface TrainHit { vec: number[]; peakDb: number; }
14export interface TrainClip {
15 id: string;
16 sr: number;
17 sens: number; // the sensitivity the onsets were detected with
18 onsets: number[]; // seconds
19 excluded: number[]; // indices into onsets
20 hits: TrainHit[]; // features of the included onsets, cached
21}
22export interface Voice {
23 id: string;
24 name: string;
25 color: string;
26 binding: Binding;
2d56c5ePer-voice playback volume applied at mix timeJeremy Magland 27 volume: number; // playback level, applied at mix time; never renders
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 28 clips: TrainClip[];
29 loose: TrainHit[]; // hits learned from takes; they belong to no clip
30}
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 31export interface TakeHit {
32 id: string; t: number; voiceId: string; gain: number;
33 peakDb: number; vec: number[] | null; // null for hand-added hits
34 s0: number; s1: number; // sample bounds in the take clip; -1 if hand-added
35}
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 36
37/** The pooled training set a voice contributes to classification. */
38export function trainPool(v: Voice): TrainHit[] {
39 return [...v.clips.flatMap((c) => c.hits), ...v.loose];
40}
42const KEY = 'instrawave-compose/v2';
43const KEY_V1 = 'instrawave-compose/v1';
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 44const KEY_IW = 'instrawave-compose/iw';
45const KEY_TAKE = 'instrawave-compose/take';
46const KEY_UI = 'instrawave-compose/ui';
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 47
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 48function read<T>(key: string): T | null {
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 49 try {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 50 const raw = localStorage.getItem(key);
51 return raw ? (JSON.parse(raw) as T) : null;
52 } catch {
53 return null;
54 }
55}
57export function loadVoices(): Voice[] {
58 const doc = read<{ voices: Voice[] }>(KEY);
60 for (const v of doc.voices) v.volume ??= 1; // pre-volume stores
61 return doc.voices;
62 }
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 63 // v1 stored a flat training list and a synth preset per voice.
64 const v1 = read<{ voices: { id: string; name: string; color: string; preset: string; train: TrainHit[] }[] }>(KEY_V1);
65 if (v1) {
66 const voices: Voice[] = v1.voices.map((v) => ({
67 id: v.id, name: v.name, color: v.color,
68 binding: { kind: 'synth', preset: v.preset as ('kick' | 'snare' | 'hat' | 'tom') },
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 70 clips: [], loose: v.train,
71 }));
72 localStorage.setItem(KEY, JSON.stringify({ voices }));
73 localStorage.removeItem(KEY_V1);
74 return voices;
75 }
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 76 return [];
77}
79export function saveVoices(voices: Voice[]): void {
80 localStorage.setItem(KEY, JSON.stringify({ voices }));
81}
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 83/** Per-instrument geometry and accuracy overrides. */
84export function loadIwOverrides(): Record<string, Record<string, number>> {
85 return read(KEY_IW) ?? {};
86}
87export function saveIwOverrides(o: Record<string, Record<string, number>>): void {
88 localStorage.setItem(KEY_IW, JSON.stringify(o));
89}
91export function loadTakeHits(): TakeHit[] | null {
92 return read(KEY_TAKE);
93}
94export function saveTakeHits(hits: TakeHit[]): void {
95 localStorage.setItem(KEY_TAKE, JSON.stringify(hits));
96}
98export interface UiState { takeSens: number; captureSens: number; }
99export function loadUi(): UiState {
100 return read(KEY_UI) ?? { takeSens: 0.6, captureSens: 0.6 };
101}
102export function saveUi(ui: UiState): void {
103 localStorage.setItem(KEY_UI, JSON.stringify(ui));
104}
7d7cc56Add overlap pursuit classifier and update composerJeremy Magland 106/** How takes are classified: one of the shipped numbl algorithms
107 * (silence_gap_knn under 'builtin', overlap_pursuit under 'pursuit',
108 * both source-editable), a custom numbl algorithm, or the original TS
b11b069Classify takes with numbl algorithms by default (silence_gap_knn from audio-spike-sorting, noise-floor adapted), with a custom-algorithm editor and the feature kNN kept as an optionJeremy Magland 109 * feature-kNN. Null sources mean "the shipped default" / "the template". */
110export interface ClassifierState {
7d7cc56Add overlap pursuit classifier and update composerJeremy Magland 111 method: 'builtin' | 'pursuit' | 'custom' | 'legacy';
b11b069Classify takes with numbl algorithms by default (silence_gap_knn from audio-spike-sorting, noise-floor adapted), with a custom-algorithm editor and the feature kNN kept as an optionJeremy Magland 112 builtinSrc: string | null;
7d7cc56Add overlap pursuit classifier and update composerJeremy Magland 113 pursuitSrc: string | null;
b11b069Classify takes with numbl algorithms by default (silence_gap_knn from audio-spike-sorting, noise-floor adapted), with a custom-algorithm editor and the feature kNN kept as an optionJeremy Magland 114 customSrc: string | null;
115}
116const KEY_CLF = 'instrawave-compose/classifier';
117export function loadClassifier(): ClassifierState {
7d7cc56Add overlap pursuit classifier and update composerJeremy Magland 118 // pursuitSrc was added later; fill it in for state saved before that
119 const c = read<Omit<ClassifierState, 'pursuitSrc'> & { pursuitSrc?: string | null }>(KEY_CLF)
120 ?? { method: 'builtin' as const, builtinSrc: null, customSrc: null };
121 return { pursuitSrc: null, ...c };
b11b069Classify takes with numbl algorithms by default (silence_gap_knn from audio-spike-sorting, noise-floor adapted), with a custom-algorithm editor and the feature kNN kept as an optionJeremy Magland 122}
123export function saveClassifier(c: ClassifierState): void {
124 localStorage.setItem(KEY_CLF, JSON.stringify(c));
125}
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 127export function clearStore(): void {
b11b069Classify takes with numbl algorithms by default (silence_gap_knn from audio-spike-sorting, noise-floor adapted), with a custom-algorithm editor and the feature kNN kept as an optionJeremy Magland 128 for (const k of [KEY, KEY_IW, KEY_TAKE, KEY_UI, KEY_CLF]) localStorage.removeItem(k);
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 129}
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 131// ---- bulk data in IndexedDB ---------------------------------------------------
838af43Per-instrument render cache in IndexedDB and .wav downloadJeremy Magland 133const STORES = ['clips', 'takes', 'bakes', 'bakemeta', 'renders'] as const;
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 134
135let dbPromise: Promise<IDBDatabase> | null = null;
136function db(): Promise<IDBDatabase> {
137 return (dbPromise ??= new Promise((resolve, reject) => {
838af43Per-instrument render cache in IndexedDB and .wav downloadJeremy Magland 138 const req = indexedDB.open('instrawave-compose', 3);
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 139 req.onupgradeneeded = () => {
140 for (const s of STORES) {
141 if (!req.result.objectStoreNames.contains(s)) req.result.createObjectStore(s);
142 }
143 };
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 144 req.onsuccess = () => resolve(req.result);
145 req.onerror = () => reject(req.error);
146 }));
147}
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 149function op<T>(store: string, mode: IDBTransactionMode, fn: (s: IDBObjectStore) => IDBRequest<T>): Promise<T> {
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 150 return db().then((d) => new Promise<T>((resolve, reject) => {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 151 const req = fn(d.transaction(store, mode).objectStore(store));
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 152 req.onsuccess = () => resolve(req.result);
153 req.onerror = () => reject(req.error);
154 }));
155}
157export function putClipAudio(id: string, samples: Float32Array, sr: number): Promise<unknown> {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 158 return op('clips', 'readwrite', (s) => s.put({ samples, sr }, id));
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 159}
160export function getClipAudio(id: string): Promise<{ samples: Float32Array; sr: number } | null> {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 161 return op<{ samples: Float32Array; sr: number } | undefined>('clips', 'readonly', (s) => s.get(id))
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 162 .then((r) => r ?? null);
163}
164export function deleteClipAudio(id: string): Promise<unknown> {
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 165 return op('clips', 'readwrite', (s) => s.delete(id));
166}
168export function putTakeAudio(samples: Float32Array, sr: number): Promise<unknown> {
169 return op('takes', 'readwrite', (s) => s.put({ samples, sr }, 'take'));
170}
171export function getTakeAudio(): Promise<{ samples: Float32Array; sr: number } | null> {
172 return op<{ samples: Float32Array; sr: number } | undefined>('takes', 'readonly', (s) => s.get('take'))
173 .then((r) => r ?? null);
174}
176/** Bake bytes and their key are separate stores so staleness can be
177 * checked without deserializing hundreds of MB. */
178export async function putBake(name: string, key: string, bytes: Uint8Array): Promise<void> {
179 await op('bakes', 'readwrite', (s) => s.put(bytes, name));
180 await op('bakemeta', 'readwrite', (s) => s.put(key, name));
181}
182export function getBakeKey(name: string): Promise<string | null> {
183 return op<string | undefined>('bakemeta', 'readonly', (s) => s.get(name)).then((r) => r ?? null);
184}
185export function getBakeBytes(name: string): Promise<Uint8Array | null> {
186 return op<Uint8Array | undefined>('bakes', 'readonly', (s) => s.get(name)).then((r) => r ?? null);
bd6f329Editable persistent trainings, multi-select exclusion, playback pulses, instrawave bindingsJeremy Magland 187}
838af43Per-instrument render cache in IndexedDB and .wav downloadJeremy Magland 189/** A rendered instrument track at the mix rate, keyed by the instrument
190 * and stamped with the exact events, tuning, and bake key it was rendered
191 * from; a matching signature means the GPU has nothing to redo. */
192export function putRender(name: string, sig: string, data: Float32Array): Promise<unknown> {
193 return op('renders', 'readwrite', (s) => s.put({ sig, data }, name));
194}
195export function getRender(name: string): Promise<{ sig: string; data: Float32Array } | null> {
196 return op<{ sig: string; data: Float32Array } | undefined>('renders', 'readonly', (s) => s.get(name))
197 .then((r) => r ?? null);
198}
2d56c5ePer-voice playback volume applied at mix timeJeremy Magland 199export function deleteRender(name: string): Promise<unknown> {
200 return op('renders', 'readwrite', (s) => s.delete(name));
201}
9f20d5bTuning audition, instrument geometry editor with 3D view, full browser persistenceJeremy Magland 203export async function clearBulk(): Promise<void> {
204 for (const s of STORES) await op(s, 'readwrite', (st) => st.clear());