/ proof-of-concept / instrawave-compose
Sign in
proof-of-concept / instrawave-compose
instrawave-compose / src / store.ts
101 lines · 3.6 KBBlameHistoryRaw
1/**
2 * Persistence. Voice metadata, training-clip descriptions, and cached
3 * feature vectors live in localStorage; the clips' raw audio lives in
4 * IndexedDB keyed by clip id, so a training can be reopened, re-detected,
5 * and re-labelled later. The two stores can drift only in one harmless
6 * direction: a clip whose audio is missing simply cannot be re-edited,
7 * while its cached features keep classifying.
8 */
9import type { Binding } from './render/types.ts';
11export interface TrainHit { vec: number[]; peakDb: number; }
12export interface TrainClip {
13 id: string;
14 sr: number;
15 sens: number; // the sensitivity the onsets were detected with
16 onsets: number[]; // seconds
17 excluded: number[]; // indices into onsets
18 hits: TrainHit[]; // features of the included onsets, cached
20export interface Voice {
21 id: string;
22 name: string;
23 color: string;
24 binding: Binding;
25 clips: TrainClip[];
26 loose: TrainHit[]; // hits learned from takes; they belong to no clip
29/** The pooled training set a voice contributes to classification. */
30export function trainPool(v: Voice): TrainHit[] {
31 return [...v.clips.flatMap((c) => c.hits), ...v.loose];
34const KEY = 'instrawave-compose/v2';
35const KEY_V1 = 'instrawave-compose/v1';
37export function loadVoices(): Voice[] {
38 try {
39 const raw = localStorage.getItem(KEY);
40 if (raw) return (JSON.parse(raw) as { voices: Voice[] }).voices;
41 const old = localStorage.getItem(KEY_V1);
42 if (old) {
43 // v1 stored a flat training list and a synth preset per voice.
44 const v1 = JSON.parse(old) as { voices: { id: string; name: string; color: string; preset: string; train: TrainHit[] }[] };
45 const voices: Voice[] = v1.voices.map((v) => ({
46 id: v.id, name: v.name, color: v.color,
47 binding: { kind: 'synth', preset: v.preset as ('kick' | 'snare' | 'hat' | 'tom') },
48 clips: [], loose: v.train,
49 }));
50 localStorage.setItem(KEY, JSON.stringify({ voices }));
51 localStorage.removeItem(KEY_V1);
52 return voices;
53 }
54 } catch { /* a corrupt store starts fresh */ }
55 return [];
58export function saveVoices(voices: Voice[]): void {
59 localStorage.setItem(KEY, JSON.stringify({ voices }));
62export function clearStore(): void {
63 localStorage.removeItem(KEY);
66// ---- clip audio in IndexedDB ------------------------------------------------
68let dbPromise: Promise<IDBDatabase> | null = null;
69function db(): Promise<IDBDatabase> {
70 return (dbPromise ??= new Promise((resolve, reject) => {
71 const req = indexedDB.open('instrawave-compose', 1);
72 req.onupgradeneeded = () => req.result.createObjectStore('clips');
73 req.onsuccess = () => resolve(req.result);
74 req.onerror = () => reject(req.error);
75 }));
78function op<T>(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => IDBRequest<T>): Promise<T> {
79 return db().then((d) => new Promise<T>((resolve, reject) => {
80 const req = fn(d.transaction('clips', mode).objectStore('clips'));
81 req.onsuccess = () => resolve(req.result);
82 req.onerror = () => reject(req.error);
83 }));
86export function putClipAudio(id: string, samples: Float32Array, sr: number): Promise<unknown> {
87 return op('readwrite', (s) => s.put({ samples, sr }, id));
90export function getClipAudio(id: string): Promise<{ samples: Float32Array; sr: number } | null> {
91 return op<{ samples: Float32Array; sr: number } | undefined>('readonly', (s) => s.get(id))
92 .then((r) => r ?? null);
95export function deleteClipAudio(id: string): Promise<unknown> {
96 return op('readwrite', (s) => s.delete(id));
99export function clearClipAudio(): Promise<unknown> {
100 return op('readwrite', (s) => s.clear());
↑↓ move↵openescclose