81 lines
3.0 KiB
TypeScript
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);
|