/** * AudioWorklet processor for VoicePaste. * * Captures raw Float32 PCM from the microphone, accumulates samples into * ~200ms frames, and posts them to the main thread for resampling + WS send. * * Communication: * Main → Processor: { command: "start" | "stop" } * Processor → Main: { type: "audio", samples: Float32Array, sampleRate: number } */ // AudioWorkletGlobalScope globals (not in standard lib) declare const sampleRate: number; declare class AudioWorkletProcessor { readonly port: MessagePort; constructor(); process( inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record, ): boolean; } declare function registerProcessor( name: string, ctor: new () => AudioWorkletProcessor, ): void; class VoicePasteProcessor extends AudioWorkletProcessor { private recording = false; private buffer: Float32Array[] = []; private bufferLen = 0; private readonly frameSize: number; constructor() { super(); // ~200ms worth of samples at current sample rate this.frameSize = Math.floor(sampleRate * 0.2); this.port.onmessage = (e: MessageEvent) => { if (e.data.command === "start") { this.recording = true; this.buffer = []; this.bufferLen = 0; } else if (e.data.command === "stop") { if (this.bufferLen > 0) { this.flush(); } this.recording = false; } }; } process(inputs: Float32Array[][]): boolean { if (!this.recording) return true; const input = inputs[0]; if (!input || !input[0]) return true; const channelData = input[0]; this.buffer.push(new Float32Array(channelData)); this.bufferLen += channelData.length; if (this.bufferLen >= this.frameSize) { this.flush(); } return true; } private flush(): void { const merged = new Float32Array(this.bufferLen); let offset = 0; for (const chunk of this.buffer) { merged.set(chunk, offset); offset += chunk.length; } this.port.postMessage( { type: "audio", samples: merged, sampleRate: sampleRate }, [merged.buffer], ); this.buffer = []; this.bufferLen = 0; } } registerProcessor("audio-processor", VoicePasteProcessor);