124 lines
4.3 KiB
JavaScript
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); |