ChatTASTE-Voice-Bot / frontend /js /audio-processor.js
YC-Chen's picture
add frontend files
a445583
/**
* 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);