/** * BufferWorkerClient * * Main-thread API for the centralized BufferWorker. * Provides promise-based reads and fire-and-forget writes. */ import type { LayerId, BufferWorkerConfig, BufferWorkerResponse, HasSpeechResult, RangeResult, BufferState, } from './types'; export class BufferWorkerClient { private worker: Worker; private messageId = 0; private pendingPromises = new Map void; reject: (e: any) => void }>(); private ready = false; constructor() { this.worker = new Worker(new URL('./buffer.worker.ts', import.meta.url), { type: 'module', }); this.worker.onmessage = (e: MessageEvent) => { this.handleMessage(e.data); }; this.worker.onerror = (e: Event) => { const err = e as ErrorEvent; console.error('[BufferWorkerClient] Worker error:', err.message); for (const [, p] of this.pendingPromises) { p.reject(new Error(err.message || 'BufferWorker error')); } this.pendingPromises.clear(); }; } // ---- Lifecycle ---- async init(config: BufferWorkerConfig): Promise { await this.sendRequest('INIT', config); this.ready = true; } async reset(): Promise { await this.sendRequest('RESET', undefined); } dispose(): void { this.worker.terminate(); for (const [, p] of this.pendingPromises) { p.reject(new Error('BufferWorkerClient disposed')); } this.pendingPromises.clear(); this.ready = false; } // ---- Producers (fire-and-forget for low latency) ---- /** * Write a single scalar value to a VAD layer (energyVad or inferenceVad). * Fire-and-forget for minimal latency. */ writeScalar(layer: LayerId, value: number): void { if (!this.ready) return; this.worker.postMessage({ type: 'WRITE', payload: { layer, data: [value] }, }); } /** * Write a multi-dimensional entry (e.g., mel spectrogram frame). * Transfers the buffer for zero-copy. */ writeEntry(layer: LayerId, data: Float32Array): void { if (!this.ready) return; const copy = new Float32Array(data); this.worker.postMessage( { type: 'WRITE', payload: { layer, data: copy } }, [copy.buffer] ); } /** * Write a batch of entries to a layer. Transfers the buffer. */ writeBatch(layer: LayerId, data: Float32Array, globalSampleOffset?: number): void { if (!this.ready) return; const copy = new Float32Array(data); this.worker.postMessage( { type: 'WRITE_BATCH', payload: { layer, data: copy, globalSampleOffset } }, [copy.buffer] ); } /** * Write a batch of entries to a layer by transferring ownership of the buffer. * The caller must not reuse `data` after calling this. */ writeBatchTransfer(layer: LayerId, data: Float32Array, globalSampleOffset?: number): void { if (!this.ready) return; this.worker.postMessage( { type: 'WRITE_BATCH', payload: { layer, data, globalSampleOffset } }, [data.buffer] ); } /** * Write raw audio samples. Fire-and-forget with buffer transfer. */ writeAudio(samples: Float32Array): void { if (!this.ready) return; const copy = new Float32Array(samples); this.worker.postMessage( { type: 'WRITE_BATCH', payload: { layer: 'audio' as LayerId, data: copy } }, [copy.buffer] ); } // ---- Consumers (async queries) ---- /** * Check if any VAD entry exceeds a threshold in a sample range. * Used by v4Tick to decide whether to trigger transcription. */ async hasSpeech( layer: 'energyVad' | 'inferenceVad', startSample: number, endSample: number, threshold: number, ): Promise { return this.sendRequest('HAS_SPEECH', { layer, startSample, endSample, threshold }); } /** * Get the duration of trailing silence from the write head. * Scans backward in the specified VAD layer until a probability >= threshold is found. */ async getSilenceTailDuration( layer: 'energyVad' | 'inferenceVad', threshold: number, ): Promise { const result = await this.sendRequest('GET_SILENCE_TAIL', { layer, threshold }); return result.durationSec; } /** * Query data for an arbitrary sample range across multiple layers. * Returns correlated slices from each requested layer. */ async queryRange( startSample: number, endSample: number, layerIds: LayerId[], ): Promise { return this.sendRequest('QUERY_RANGE', { startSample, endSample, layers: layerIds }); } /** * Get a snapshot of the buffer state for debugging / UI. */ async getState(): Promise { return this.sendRequest('GET_STATE', undefined); } // ---- Internal ---- private handleMessage(msg: BufferWorkerResponse): void { if (msg.type === 'ERROR') { const p = this.pendingPromises.get(msg.id); if (p) { this.pendingPromises.delete(msg.id); p.reject(new Error(msg.payload)); } return; } if (msg.id !== undefined) { const p = this.pendingPromises.get(msg.id); if (p) { this.pendingPromises.delete(msg.id); p.resolve(msg.payload); } } } private sendRequest(type: string, payload: any): Promise { return new Promise((resolve, reject) => { const id = ++this.messageId; this.pendingPromises.set(id, { resolve, reject }); this.worker.postMessage({ type, payload, id }); }); } }