/** * AudioWorklet Processor for exact chunk size control * Accumulates audio samples and sends exactly 1600 samples (100ms at 16kHz) per chunk * * IMPORTANT: This processor resamples from browser's native sample rate (usually 48kHz) * down to 16kHz required by the backend. */ class AudioChunkProcessor extends AudioWorkletProcessor { constructor() { super(); this.buffer = []; // Target: 100ms at 16kHz = 1600 samples (matches VAD chunk_duration_ms) // Server accumulates 6x 100ms chunks into 600ms chunks for ASR this.targetSamples = 1600; this.targetSampleRate = 16000; // Resampling state this.resampleBuffer = []; this.inputSampleRate = sampleRate; // AudioContext's native sample rate (e.g., 48000) this.resampleRatio = this.inputSampleRate / this.targetSampleRate; // e.g., 48000/16000 = 3 } process(inputs, outputs, parameters) { const input = inputs[0]; if (!input || !input[0]) return true; const inputData = input[0]; // Float32Array from Web Audio API (at native sample rate, e.g., 48kHz) // Resample from native rate (48kHz) to target rate (16kHz) // Using simple linear interpolation decimation for (let i = 0; i < inputData.length; i++) { this.resampleBuffer.push(inputData[i]); } // Decimate: take every Nth sample (e.g., every 3rd sample for 48kHz->16kHz) while (this.resampleBuffer.length >= this.resampleRatio) { // Simple decimation: take first sample, discard rest // For better quality, could use averaging or proper low-pass filter const sample = this.resampleBuffer[0]; this.buffer.push(sample); // Remove processed samples this.resampleBuffer.splice(0, Math.floor(this.resampleRatio)); } // Send chunks when we have enough samples while (this.buffer.length >= this.targetSamples) { const chunk = this.buffer.splice(0, this.targetSamples); // Convert Float32Array to Int16Array (PCM 16-bit) const int16Data = new Int16Array(chunk.length); for (let i = 0; i < chunk.length; i++) { const s = Math.max(-1, Math.min(1, chunk[i])); int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; } // Send to main thread (transfer ownership for zero-copy) this.port.postMessage({ type: 'audio-chunk', data: int16Data.buffer, samples: this.targetSamples, timestamp: currentTime }, [int16Data.buffer]); } return true; // Keep processor alive } } registerProcessor('audio-chunk-processor', AudioChunkProcessor);