38443-vm/assets/js/rnnoise-processor.js
2026-03-30 09:50:00 +00:00

124 lines
4.3 KiB
JavaScript

// RNNoise Web Audio Worklet Processor
// Uses the RNNoise WASM binary to perform real-time noise suppression
class RNNoiseProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.alive = true;
this.wasmInstance = null;
this.rnnoise = null;
this.initialized = false;
// RNNoise expects 480 samples (10ms at 48kHz)
this.FRAME_SIZE = 480;
// Buffers
this.inputBuffer = new Float32Array(this.FRAME_SIZE);
this.outputBuffer = new Float32Array(this.FRAME_SIZE);
// Circular buffer for output to handle latency and fixed frame size
this.outCircleBuffer = new Float32Array(this.FRAME_SIZE * 2);
this.outReadPtr = 0;
this.outWritePtr = 0;
this.outCount = 0;
this.bufferPtr = 0;
this.heapInputPtr = null;
this.heapOutputPtr = null;
this.statePtr = null;
this.vadThreshold = 0.85;
this.enabled = true;
this.port.onmessage = (event) => {
if (event.data.type === 'INIT') {
this.initWasm(event.data.wasmBinary);
} else if (event.data.type === 'SET_ENABLED') {
this.enabled = event.data.enabled;
} else if (event.data.type === 'SET_THRESHOLD') {
this.vadThreshold = event.data.value;
}
};
}
async initWasm(wasmBinary) {
try {
const wasmModule = await WebAssembly.instantiate(wasmBinary, {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
abort: () => { console.error("WASM Abort"); }
}
});
this.wasmInstance = wasmModule.instance;
this.rnnoise = this.wasmInstance.exports;
this.statePtr = this.rnnoise.rnnoise_create(0);
this.heapInputPtr = this.rnnoise.malloc(this.FRAME_SIZE * 4);
this.heapOutputPtr = this.rnnoise.malloc(this.FRAME_SIZE * 4);
this.initialized = true;
console.log("RNNoise Processor Initialized");
} catch (e) {
console.error("Failed to initialize RNNoise WASM:", e);
}
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
if (!input || !input[0] || !output || !output[0]) return true;
if (!this.initialized || !this.enabled) {
output[0].set(input[0]);
return true;
}
const inputChannel = input[0];
const outputChannel = output[0];
for (let i = 0; i < inputChannel.length; i++) {
// Store input
this.inputBuffer[this.bufferPtr] = inputChannel[i] * 32768.0;
this.bufferPtr++;
if (this.bufferPtr >= this.FRAME_SIZE) {
this.processRNNoise();
this.bufferPtr = 0;
}
// Read from circular output buffer if available, else output silence/last
if (this.outCount > 0) {
outputChannel[i] = this.outCircleBuffer[this.outReadPtr];
this.outReadPtr = (this.outReadPtr + 1) % this.outCircleBuffer.length;
this.outCount--;
} else {
outputChannel[i] = 0;
}
}
return true;
}
processRNNoise() {
const heapInput = new Float32Array(this.rnnoise.memory.buffer, this.heapInputPtr, this.FRAME_SIZE);
heapInput.set(this.inputBuffer);
const vadProbability = this.rnnoise.rnnoise_process_frame(this.statePtr, this.heapOutputPtr, this.heapInputPtr);
const heapOutput = new Float32Array(this.rnnoise.memory.buffer, this.heapOutputPtr, this.FRAME_SIZE);
// Write to circular output buffer
for (let i = 0; i < this.FRAME_SIZE; i++) {
let sample = heapOutput[i] / 32768.0;
// Apply gate
if (vadProbability < this.vadThreshold) sample = 0;
this.outCircleBuffer[this.outWritePtr] = sample;
this.outWritePtr = (this.outWritePtr + 1) % this.outCircleBuffer.length;
this.outCount++;
}
}
}
registerProcessor('rnnoise-processor', RNNoiseProcessor);