132 lines
3.9 KiB
JavaScript
132 lines
3.9 KiB
JavaScript
/**
|
|
* MicProcessor AudioWorkletProcessor
|
|
*
|
|
* Captures microphone audio at the browser's native sample rate,
|
|
* resamples to 16kHz mono via linear interpolation, and emits
|
|
* 960-sample Int16 frames (30ms at 16kHz) to the main thread.
|
|
*
|
|
* Wire format (ArrayBuffer):
|
|
* [0x01][seq: 4B big-endian][pcm: 960 * 2 bytes = 1920 bytes Int16]
|
|
* Total frame size: 1925 bytes
|
|
*/
|
|
|
|
const TARGET_SAMPLE_RATE = 16000;
|
|
const FRAME_SAMPLES = 960; // 30ms at 16kHz
|
|
const HEADER_BYTES = 5; // 1 type byte + 4 seq bytes
|
|
const FRAME_BYTES = FRAME_SAMPLES * 2; // Int16 = 2 bytes per sample
|
|
const BUFFER_BYTES = HEADER_BYTES + FRAME_BYTES;
|
|
|
|
class MicProcessor extends AudioWorkletProcessor {
|
|
constructor() {
|
|
super();
|
|
|
|
/** Resampled PCM accumulation buffer */
|
|
this._accumulator = new Float32Array(FRAME_SAMPLES * 2);
|
|
/** Write position in accumulator */
|
|
this._accPos = 0;
|
|
/** Fractional sample position for linear interpolation resampling */
|
|
this._resamplePhase = 0.0;
|
|
/** Per-frame sequence counter (uint32, wraps) */
|
|
this._seq = 0;
|
|
/** Ratio: input samples per output sample */
|
|
this._ratio = 0;
|
|
}
|
|
|
|
/**
|
|
* Linear interpolation resampler.
|
|
* Takes a block of float32 input samples (at native rate) and appends
|
|
* resampled float32 output samples (at 16kHz) into the accumulator,
|
|
* flushing complete 960-sample frames to the main thread.
|
|
*
|
|
* @param {Float32Array} input - Input samples at native sample rate
|
|
*/
|
|
_resampleAndAccumulate(input) {
|
|
const inputLen = input.length;
|
|
if (inputLen === 0) return;
|
|
|
|
// Compute ratio on first real input block (sampleRate is available in worklet)
|
|
if (this._ratio === 0) {
|
|
this._ratio = sampleRate / TARGET_SAMPLE_RATE;
|
|
}
|
|
|
|
const ratio = this._ratio;
|
|
let phase = this._resamplePhase;
|
|
|
|
while (phase < inputLen) {
|
|
const i0 = Math.floor(phase);
|
|
const i1 = Math.min(i0 + 1, inputLen - 1);
|
|
const frac = phase - i0;
|
|
|
|
const s0 = input[i0];
|
|
const s1 = input[i1];
|
|
const sample = s0 + frac * (s1 - s0);
|
|
|
|
this._accumulator[this._accPos++] = sample;
|
|
|
|
if (this._accPos === FRAME_SAMPLES) {
|
|
this._flushFrame();
|
|
this._accPos = 0;
|
|
}
|
|
|
|
phase += ratio;
|
|
}
|
|
|
|
// Carry over fractional phase (subtract consumed integer samples)
|
|
this._resamplePhase = phase - inputLen;
|
|
}
|
|
|
|
/**
|
|
* Encode and post a complete 960-sample frame.
|
|
*/
|
|
_flushFrame() {
|
|
const buffer = new ArrayBuffer(BUFFER_BYTES);
|
|
const view = new DataView(buffer);
|
|
|
|
// Header
|
|
view.setUint8(0, 0x01);
|
|
view.setUint32(1, this._seq, false); // big-endian
|
|
|
|
// PCM: Float32 → Int16 with clamping.
|
|
// Int16Array requires 2-byte-aligned offsets; HEADER_BYTES=5 is odd, so use a
|
|
// separate aligned buffer and copy the bytes in with Uint8Array.
|
|
const pcmTemp = new Int16Array(FRAME_SAMPLES);
|
|
for (let i = 0; i < FRAME_SAMPLES; i++) {
|
|
const f = this._accumulator[i];
|
|
const clamped = f < -1.0 ? -1.0 : f > 1.0 ? 1.0 : f;
|
|
pcmTemp[i] = Math.round(clamped * 32767);
|
|
}
|
|
new Uint8Array(buffer, HEADER_BYTES).set(new Uint8Array(pcmTemp.buffer));
|
|
|
|
this.port.postMessage(buffer, [buffer]);
|
|
this._seq = (this._seq + 1) >>> 0; // unsigned 32-bit increment
|
|
}
|
|
|
|
/**
|
|
* @param {Float32Array[][]} inputs
|
|
* @returns {boolean}
|
|
*/
|
|
process(inputs) {
|
|
const input = inputs[0];
|
|
if (!input || input.length === 0) return true;
|
|
|
|
// Mix down to mono if stereo
|
|
const ch0 = input[0];
|
|
if (!ch0 || ch0.length === 0) return true;
|
|
|
|
let mono;
|
|
if (input.length > 1 && input[1] && input[1].length === ch0.length) {
|
|
mono = new Float32Array(ch0.length);
|
|
const ch1 = input[1];
|
|
for (let i = 0; i < ch0.length; i++) {
|
|
mono[i] = (ch0[i] + ch1[i]) * 0.5;
|
|
}
|
|
} else {
|
|
mono = ch0;
|
|
}
|
|
|
|
this._resampleAndAccumulate(mono);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
registerProcessor('mic-processor', MicProcessor);
|