/** * PCM Recorder AudioWorklet. * * Captures mono Float32 mic samples at the AudioContext's native rate, * resamples them to a target sample rate (default 16 kHz) with linear * interpolation, then ships PCM16 frames of a fixed duration (default 20 ms) * to the main thread via `port.postMessage(ArrayBuffer)`. * * It also computes a simple RMS level per frame for the UI VU meter so the * main thread doesn't have to re-process the audio. */ class PcmRecorderProcessor extends AudioWorkletProcessor { constructor(options) { super(); const opts = (options && options.processorOptions) || {}; this._targetSampleRate = opts.targetSampleRate || 16000; this._frameMs = opts.frameMs || 20; this._frameSamples = Math.round( (this._targetSampleRate * this._frameMs) / 1000, ); // Resampling state. // `ratio` is input samples per output sample. this._ratio = sampleRate / this._targetSampleRate; this._inputBuffer = new Float32Array(0); // Float position in `_inputBuffer` for the next output sample. this._inputOffset = 0; // Output framing state. this._frameBuffer = new Int16Array(this._frameSamples); this._frameIndex = 0; // VU meter accumulator. this._rmsSumSquares = 0; this._rmsCount = 0; } process(inputs) { const input = inputs[0]; if (!input || input.length === 0) return true; const channel = input[0]; if (!channel || channel.length === 0) return true; // Append new samples to the input buffer. const merged = new Float32Array(this._inputBuffer.length + channel.length); merged.set(this._inputBuffer, 0); merged.set(channel, this._inputBuffer.length); this._inputBuffer = merged; const ratio = this._ratio; const inLen = this._inputBuffer.length; let pos = this._inputOffset; while (pos + 1 < inLen) { const lo = Math.floor(pos); const hi = lo + 1; const w = pos - lo; const sample = this._inputBuffer[lo] * (1 - w) + this._inputBuffer[hi] * w; this._rmsSumSquares += sample * sample; this._rmsCount += 1; let s = sample; if (s > 1) s = 1; else if (s < -1) s = -1; this._frameBuffer[this._frameIndex++] = s < 0 ? Math.round(s * 0x8000) : Math.round(s * 0x7fff); if (this._frameIndex === this._frameSamples) { const frame = new Int16Array(this._frameSamples); frame.set(this._frameBuffer); const rms = this._rmsCount > 0 ? Math.sqrt(this._rmsSumSquares / this._rmsCount) : 0; this.port.postMessage( { type: "frame", buffer: frame.buffer, rms }, [frame.buffer], ); this._frameIndex = 0; this._rmsSumSquares = 0; this._rmsCount = 0; } pos += ratio; } // Trim consumed samples from the input buffer; keep at least the last // sample we still need to interpolate against on the next call. const consumed = Math.floor(pos); if (consumed > 0) { this._inputBuffer = this._inputBuffer.slice(consumed); pos -= consumed; } this._inputOffset = pos; return true; } } registerProcessor("pcm-recorder", PcmRecorderProcessor);