// AudioWorkletProcessor: downsample mic input to 16 kHz mono **Int16 LE** PCM. // Qwen3-ASR streaming server (mochi-asr on i7:9000) expects Int16 LE. // // Browsers run AudioContext at 44.1k or 48k. We linear-resample to 16k. const TARGET_RATE = 16000; const CHUNK_SAMPLES = 1600; // 100 ms at 16 kHz class AsrWorklet extends AudioWorkletProcessor { constructor() { super(); this._ratio = sampleRate / TARGET_RATE; this._tail = []; } process(inputs) { const input = inputs[0]; if (!input || input.length === 0 || !input[0]) return true; const channel = input[0]; const outLen = Math.floor(channel.length / this._ratio); for (let i = 0; i < outLen; i++) { const srcIdx = i * this._ratio; const i0 = srcIdx | 0; const i1 = i0 + 1 < channel.length ? i0 + 1 : channel.length - 1; const t = srcIdx - i0; this._tail.push(channel[i0] * (1 - t) + channel[i1] * t); } while (this._tail.length >= CHUNK_SAMPLES) { const samples = this._tail.splice(0, CHUNK_SAMPLES); const i16 = new Int16Array(CHUNK_SAMPLES); for (let i = 0; i < CHUNK_SAMPLES; i++) { let s = samples[i]; if (s < -1) s = -1; else if (s > 1) s = 1; i16[i] = s < 0 ? Math.round(s * 32768) : Math.round(s * 32767); } this.port.postMessage(i16.buffer, [i16.buffer]); } return true; } } registerProcessor('asr-worklet', AsrWorklet);