/** * 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 } */ class AudioProcessor extends AudioWorkletProcessor { constructor() { super(); this.recording = false; this.buffer = []; this.bufferLen = 0; // ~200ms worth of samples at current sample rate // sampleRate is a global in AudioWorkletGlobalScope this.frameSize = Math.floor(sampleRate * 0.2); this.port.onmessage = (e) => { if (e.data.command === "start") { this.recording = true; this.buffer = []; this.bufferLen = 0; } else if (e.data.command === "stop") { // Flush remaining samples if (this.bufferLen > 0) { this._flush(); } this.recording = false; } }; } process(inputs) { if (!this.recording) return true; const input = inputs[0]; if (!input || !input[0]) return true; // Mono channel 0 const channelData = input[0]; this.buffer.push(new Float32Array(channelData)); this.bufferLen += channelData.length; if (this.bufferLen >= this.frameSize) { this._flush(); } return true; } _flush() { // Merge buffer chunks into a single Float32Array 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] // Transfer ownership for zero-copy ); this.buffer = []; this.bufferLen = 0; } } registerProcessor("audio-processor", AudioProcessor);