Webassembly is an approach of choice when speed is required in web page script processing. The audioWorklet inner loop requiries speed for a fast processing of the continuous sample batches flowing through webAudio AudioNode chain.
The aim of this project is to implement a C version of the well known Moog low pass Ladder filter as a webAudio audioWorkletNode. Such a digital filter is CPU intensive so a wasm module is a great candidate. Interface between javascript and the native inner function will be as thin as possible.
> screenshot- /src/minimal : barebone osc + nop filter + speaker
- /src/ladder : moog ladder audioWorktetNode osc + lader filter + speaker
- /src/demo : add resonance & offset tunable parameters through sliders + oscilloscope for visualization Thanks to ddiakopoulos/MoogLadders for it great study of existing moog filters. The microtracker works well for me.
Compiles C source file to wasm module. No needs for emscripten js glue to keep things as small and simple as posible.
emcc -O3 -s WASM=1 filterKernel.c -o filterKernel.wasm --no-entry
- WASM=1 : will output a Webassedmbly module
- --no-entry : no main function to export.
actualy the full wasm byteCode is 1123 bytes including the Moog Lader filter code !
#include <emscripten.h>
...
float inputBuffer[128];
float outputBuffer[128];
...
EMSCRIPTEN_KEEPALIVE
float* inputBufferPtr() {
return inputBuffer;
}
EMSCRIPTEN_KEEPALIVE
float* outputBufferPtr() {
return outputBuffer;
}
EMSCRIPTEN_KEEPALIVE
void filter() {
for (int i=0 ; i<128 ; i++) {
...
outputBuffer[i] = out;
}
}
- the EMSCRIPTEN_KEEPALIVE macro "Tells the compiler and linker to preserve a symbol, and export it" [emscripten]
- float inputBuffer[128] & float outputBuffer[128] creates two local memory float buffers.
- inputBufferPtr() & outputBufferPtr() are two exported functions returning pointers to the allocated local memory.
- Creates AudioWorkletNode
- Reads wasm byteCode as a byteArray
- Sends the byteCode to the newly created AudioWorkletProcessor
// Creates a AudioWorkletNode and its associated AudioWorkletProcessor
await audioCtx.audioWorklet.addModule('worklet-processor.js')
filterWorkletNode = new AudioWorkletNode(audioCtx, 'worklet-processor')
// Gets WeAssembly byteCode from file
const response = await fetch('filterKernel.wasm')
const byteCode = await response.arrayBuffer()
// Sends bytecode to the AudioWorkletProcessor for instanciation
filterWorkletNode.port.postMessage(byteCode)
- instantiate the received byteCode, resulting in a module and the first instance of that module.
- get pointers to instance memory.
- create a javascript shadow buffer pointing to the corresponding instance buffer.
- create the innerloop samples process as native code function
this.port.onmessage = (e) => {
// Instanciate
WebAssembly.instantiate(e.data) // 1.
.then((result) => {
/* result : {module: Module, instance: Instance} */
// exposes C functions to the outside world. only for readness
const exports = result.instance.exports;
// Gets pointer to wasm module memory
this.inputStart = exports.inputBufferPtr(); //2.
this.outputStart = exports.outputBufferPtr();
// Create shadow typed buffer of float.
this.inputBuffer = new Float32Array(exports.memory.buffer, //3.
this.inputStart,
this.WABEAUDIO_FRAME_SIZE);
this.outputBuffer = new Float32Array(exports.memory.buffer,
this.outputStart,
this.WABEAUDIO_FRAME_SIZE);
// Gets the filter function
this.filter = exports.filter; //4.
});
- filter: ƒ $filter() -> filter function
- inputBufferPtr: ƒ $inputBufferPtr() -> return buffer ptr function
- memory: Memory(256) -> Wasm memory : 256 page
*buffer: ArrayBuffer(16777216) -> WebAssembly pages are 1024
*[[Prototype]]: WebAssembly.Memory
- outputBufferPtr: ƒ $outputBufferPtr() -> return buffer ptr function
- ...
- copy webAudio samples buffer to local memory
- process samples (ie. audio filter)
- returns processed samples to WebAudio next Node
...
process(inputList, outputList, parameters) {
this.inputBuffer.set(inputList[0][0]); // 1.
this.filter(); // 2.
outputList[0][0].set(this.outputBuffer); // 3.
return true;
}
...
registerProcessor('worklet-processor', WorkletProcessor);
Filters needs to be parameterized. Hereafter two transmitting chains between main javascript and inner samples processor loop.
- UI sends message to WASMWorkletProcessor
document.getElementById('cutOff').addEventListener('input', (evt) => {
ladderNode.port.postMessage({cutOff: evt.target.value})
});
- WASMWorkletProcessor calls an wasm exported function
...
// a 'shortcut' to the exported C function
this.setCutoff = exports.setCutoff;
...
this.port.onmessage = (e) => {
...
// calls the C function to set the value
this.setCutoff(value);
...
}
- Wasm filter process set local variable
float cutoff;
EMSCRIPTEN_KEEPALIVE
void setCutoff(float c){
cutoff = c * 2 * _PI / _SAMPLERATE;
cutoff = (cutoff > 1) ? 1 : cutoff;
}
- Declare a parmeter in the audioWorkletProcessor static get parameterDescriptors() function
static get parameterDescriptors() {
return [
{
name: "Q",
defaultValue: 1.0,
minValue: 0.02,
maxValue: 2.0,
automationRate: "k-rate",
},
];
}
- Instanciate the parameter in the UI
QParam = ladderNode.parameters.get("Q");
- Use the created parameter in the UI as a regular WebAudio parameter.
document.getElementById('resonance').addEventListener('input', (evt) => {
QParam.value = parseFloat(evt.target.value)/20.0;
});