Files
vocero-client/s2s-frontend/src/audio/mic-capture.worklet.ts
T

81 lines
3.0 KiB
TypeScript

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<Control>) => {
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);