const TARGET_RATE = 16000; type Control = { kind: "enable"; value: boolean } | { kind: "gate"; enabled: boolean; thresholdDb: number }; /** Preserve the existing 40 ms PCM16 frames and smoothed gate envelope. */ class MicCaptureProcessor extends AudioWorkletProcessor { private readonly ratio = sampleRate / TARGET_RATE; private readonly chunkSize = 640; private scratch = new Float32Array(0); private readonly decimated = new Float32Array(this.chunkSize); private enabled = true; private gateEnabled = false; private threshold = 0; private gain = 1; private hold = 0; private readonly attack = Math.exp(-1 / (0.005 * TARGET_RATE)); private readonly release = Math.exp(-1 / (0.08 * TARGET_RATE)); constructor() { super(); this.port.onmessage = (event: MessageEvent) => { if (event.data.kind === "enable") { this.enabled = event.data.value; } else { this.gateEnabled = event.data.enabled; this.threshold = event.data.enabled ? 10 ** (event.data.thresholdDb / 20) : 0; } }; } process(inputs: Float32Array[][]) { const incoming = inputs[0]?.[0]; if (!incoming?.length) { return true; } const next = new Float32Array(this.scratch.length + incoming.length); next.set(this.scratch); next.set(incoming, this.scratch.length); this.scratch = next; const needed = Math.ceil(this.chunkSize * this.ratio); while (this.scratch.length >= needed) { let sum = 0; for (let index = 0; index < this.chunkSize; index++) { const position = index * this.ratio; const start = Math.floor(position); const a = this.scratch[start] ?? 0; const value = Math.abs(this.ratio - 3) < 1e-6 ? (a + (this.scratch[start + 1] ?? 0) + (this.scratch[start + 2] ?? 0)) / 3 : a + ((this.scratch[start + 1] ?? a) - a) * (position - start); this.decimated[index] = value; sum += value * value; } const rms = Math.sqrt(sum / this.chunkSize); let target = 1; if (this.gateEnabled) { if (rms >= this.threshold) { this.hold = TARGET_RATE * 0.25; } else if (this.hold > 0) { this.hold -= this.chunkSize; } else { target = 0; } } const output = new Int16Array(this.chunkSize); for (let index = 0; index < this.chunkSize; index++) { const coefficient = target > this.gain ? this.attack : this.release; this.gain = target + (this.gain - target) * coefficient; const value = Math.max(-1, Math.min(1, (this.decimated[index] ?? 0) * this.gain)); output[index] = value < 0 ? value * 0x8000 : value * 0x7fff; } this.scratch = this.scratch.slice(Math.floor(this.chunkSize * this.ratio)); this.port.postMessage({ kind: "level", rms }); if (this.enabled) { this.port.postMessage(output.buffer, [output.buffer]); } } return true; } } registerProcessor("mic-capture", MicCaptureProcessor);