Files
alvaro abd03ed07d refactor: rename API and frontend projects to Vocero
Rename the project directories, Python distribution and import package, and npm package. Update launcher, Docker, tooling, tests, lockfiles, and documentation while preserving existing API contracts.

Make AGENTS.md self-contained and remove external skill and workflow references.
2026-10-06 19:26:55 -03:00

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);