How AudioWorkletNode Executes Custom Audio Algorithms
The AudioWorkletNode executes custom audio synthesis
algorithms by offloading audio processing from the main JavaScript UI
thread to a dedicated, low-latency audio rendering thread. By
implementing an AudioWorkletProcessor, developers can
directly manipulate raw audio sample buffers in real-time using custom
Digital Signal Processing (DSP) code. This decoupled architecture
ensures glitch-free, high-performance audio synthesis while maintaining
seamless bidirectional communication with the main application
thread.
The Two-Thread Architecture
Executing custom DSP algorithms in the Web Audio API requires a strict separation of concerns across two threads:
- Main Thread (
AudioWorkletNode): Represents the interface inside the standard web application. It handles instantiation, routing inside the audio graph, and UI-driven parameter updates. - Audio Rendering Thread
(
AudioWorkletProcessor): Operates in a separateAudioWorkletGlobalScope. It continuously executes the synthesis math inside a synchronous rendering loop without interference from DOM updates, garbage collection spikes, or main-thread JavaScript execution.
Step 1: Defining the
AudioWorkletProcessor
To create a synthesis algorithm, you define a class that extends
AudioWorkletProcessor within a separate JavaScript file.
This processor must define a process() method and register
itself using the global registerProcessor function.
// custom-oscillator.js
class CustomOscillatorProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.phase = 0;
}
static get parameterDescriptors() {
return [
{
name: 'frequency',
defaultValue: 440,
minValue: 20,
maxValue: 20000,
automationRate: 'a-rate'
}
];
}
process(inputs, outputs, parameters) {
const output = outputs[0];
const frequency = parameters.frequency;
const isConstant = frequency.length === 1;
// Standard Web Audio render quantum is 128 frames
for (let channel = 0; channel < output.length; ++channel) {
const outputChannel = output[channel];
for (let i = 0; i < outputChannel.length; ++i) {
const currentFreq = isConstant ? frequency[0] : frequency[i];
// Custom synthesis algorithm: Sine wave generation
outputChannel[i] = Math.sin(this.phase);
// Advance phase according to sample rate
this.phase += (2 * Math.PI * currentFreq) / sampleRate;
if (this.phase >= 2 * Math.PI) {
this.phase -= 2 * Math.PI;
}
}
}
// Returning true keeps the processor alive in the audio graph
return true;
}
}
registerProcessor('custom-oscillator', CustomOscillatorProcessor);Step 2: The
process() Execution Loop
The audio engine invokes the process() method
periodically in chunks called render quanta (typically 128 sample frames
per block).
- Inputs and Outputs: Received as multidimensional
arrays (
[inputIndex][channelIndex][sampleIndex]). For synthesis, output arrays are populated directly with calculated numerical values between-1.0and1.0. - Sample Rate: The global
sampleRateproperty provides the current context’s sample frequency (e.g., 44100 or 48000 Hz) to calculate precise sample-by-sample phase increments. - Return Value: Returning
trueinstructs the audio engine to keep the node active; returningfalsemarks it for garbage collection once buffers clear.
Step 3:
Sample-Accurate Modulation with AudioParam
The static getter parameterDescriptors() exposes custom
parameters to the Web Audio clock. In the process() method:
- a-rate Parameters: Provide an array of
128 values per render quantum, allowing sample-accurate automation
(e.g., smooth frequency ramps or envelopes). -
k-rate Parameters: Provide a single value
per quantum for low-frequency changes, reducing computational
overhead.
Step 4: Loading and Connecting on the Main Thread
To execute the algorithm, the main thread imports the processor
script into the audio context’s worklet module, instantiates the
AudioWorkletNode, and connects it to the graph
destination.
const audioContext = new AudioContext();
// 1. Load the processor script into the audio rendering thread
await audioContext.audioWorklet.addModule('custom-oscillator.js');
// 2. Instantiate the node referencing the registered processor name
const synthNode = new AudioWorkletNode(audioContext, 'custom-oscillator');
// 3. Modulate parameters or connect to speakers
synthNode.parameters.get('frequency').setValueAtTime(880, audioContext.currentTime);
synthNode.connect(audioContext.destination);Bidirectional Communication
Non-audio data (such as custom wavetables, filter coefficients, or
trigger events) is passed between the AudioWorkletNode and
AudioWorkletProcessor via a built-in
MessagePort. Both sides expose a port object
(node.port on the main thread and this.port on
the processor) capable of sending and receiving serialized data
asynchronously through postMessage() and
onmessage handlers.