Under the Hood
Webaudio

AudioWorklet: your own DSP on the audio thread

When the built-in nodes run out of road, AudioWorklet lets you register your own sample-processing class and have the browser call it once per render quantum on the real-time audio thread itself, which is the deliberate payoff of everything this module has said about that thread's deadline.

AudioWorklet: your own DSP on the audio thread

Every node this module has covered — oscillators, gain, biquad filters, delay, convolution, the analyser — is a built-in the audio thread evaluates natively, in compiled code, fast enough to never miss its ~3 ms deadline. None of them required you to write a single line of per-sample math. AudioWorklet is what you reach for when that stops being enough: when you need a sound-processing algorithm the built-ins simply don't offer, and you're willing to write it yourself, sample by sample, running directly on the audio thread lesson 1 told you never to touch.

Defining a processor

An AudioWorkletProcessor is a class with one method that matters: process(inputs, outputs, parameters). The browser calls it once per render quantum — the same 128-sample block from lesson 1 — and your job is to fill the output channel arrays with 128 fresh samples before it returns.

// noise-processor.js — runs on the audio thread, in its own worklet global scope
class WhiteNoiseProcessor extends AudioWorkletProcessor {
  process(inputs, outputs, parameters) {
    const output = outputs[0];
    for (let channel = 0; channel < output.length; channel++) {
      const samples = output[channel];
      for (let i = 0; i < samples.length; i++) {
        samples[i] = Math.random() * 2 - 1; // one fresh sample, every call
      }
    }
    return true; // keep this processor alive for the next quantum
  }
}

registerProcessor("white-noise", WhiteNoiseProcessor);

That inner loop is the payoff of the whole module's framing. Every earlier lesson described you as the patch-bay operator, wiring and configuring nodes while the audio thread did the actual per-sample work natively. Inside process(), for the first time, you are that per-sample work — this is where you finally stand in the hot path lesson 1 promised you'd only enter by deliberate choice.

Wiring a worklet into the graph

Getting a processor running takes three steps, and the first one is asynchronous because it loads a separate module file:

const context = new AudioContext();

await context.audioWorklet.addModule("noise-processor.js");

const noiseNode = new AudioWorkletNode(context, "white-noise");
noiseNode.connect(context.destination);

addModule fetches and evaluates noise-processor.js inside the worklet's own global scope — not the main thread, not quite the same scope other audio-thread code runs in either, but a dedicated JS realm the audio thread hosts specifically for your registered processors. Once registered, new AudioWorkletNode(context, "white-noise") creates a normal-looking node you drop into the graph exactly like any built-in: .connect() it to a filter, a gain, the destination, anywhere a node is expected.

The real-time constraints inside process()

process() runs under the same deadline every built-in node meets, but now the discipline is yours to keep rather than the browser's. Every render quantum is due roughly every 2.9 ms, and if your process() call doesn't return in time, the audio thread has nothing to hand the speakers and you get the same click or dropout lesson 1 described for a missed deadline anywhere else in the graph.

That means the same rules real-time audio programmers have always lived by now apply to your JavaScript: no allocation, no blocking, no garbage-collector-heavy work inside process(). Allocating a new array every call (rather than reusing one created once, outside the loop) risks a GC pause landing inside your ~3 ms window; blocking calls (synchronous I/O, anything that waits) stall the one thread with zero tolerance for stalling. The discipline the flagship lesson established as "why audio needs its own thread at all" becomes, here, a discipline you personally have to uphold every time process() is called.

Talking to the main thread

A worklet processor runs in isolation on the audio thread, but it still needs to hear from — and sometimes talk back to — the main thread. Two mechanisms cover it:

  • MessagePort — every AudioWorkletProcessor gets this.port, and every AudioWorkletNode gets a matching .port, wired together automatically. this.port.postMessage(data) inside process() sends data out to the main thread (a metering value, a detected onset), and node.port.onmessage receives it there; the reverse direction works the same way, letting the main thread hand configuration into the processor asynchronously.
  • parameterDescriptors — a static getter you define alongside your class, declaring custom AudioParams the main thread can schedule exactly like any built-in node's params (lesson 4's setValueAtTime, ramps, and all). Inside process(), the third argument (parameters) hands you the current value of each declared param, already resolved for this render quantum.
class GainRampProcessor extends AudioWorkletProcessor {
  static get parameterDescriptors() {
    return [{ name: "amount", defaultValue: 1, automationRate: "a-rate" }];
  }

  process(inputs, outputs, parameters) {
    const input = inputs[0];
    const output = outputs[0];
    const amount = parameters.amount; // an array: one value per sample if a-rate

    for (let channel = 0; channel < output.length; channel++) {
      for (let i = 0; i < output[channel].length; i++) {
        const gain = amount.length > 1 ? amount[i] : amount[0];
        output[channel][i] = input[channel][i] * gain;
      }
    }
    return true;
  }
}

Why this exists: replacing ScriptProcessorNode

AudioWorklet has a direct predecessor worth naming, because the history is the mechanism's justification. Before AudioWorklet, custom DSP meant ScriptProcessorNode, which ran your callback on the main thread and handed it a buffer of samples to fill — the exact thing lesson 1 warned about, except for audio processing specifically. Under any main-thread load — a heavy layout pass, a garbage-collection pause, a synchronous script — ScriptProcessorNode's callback would miss its deadline and the audio would glitch, because the thread doing your DSP was the same unreliable thread doing everything else in the page. ScriptProcessorNode is now deprecated for exactly this reason. AudioWorklet fixes it by moving custom processing to the same dedicated, high-priority audio thread every built-in node already ran on — the fix isn't a smarter callback, it's relocating the work to the thread that was always the point of this module.

WebAssembly inside a worklet

Because process() is a genuine per-sample hot loop, it's also exactly the place where WebAssembly's speed advantage pays off most directly. A worklet processor is ordinary JavaScript, but nothing stops it from calling into a compiled WASM module for the actual number-crunching — compile a DSP kernel written in C or Rust to .wasm, instantiate it once in the processor's constructor, and call an exported function from inside process() to do the heavy per-sample math at near-native speed, while JavaScript still handles the wiring and the AudioWorkletProcessor scaffolding around it. It's the same JS-calls-into-compiled-code shape the WebAssembly module covers generally, applied to the one place in this whole API where per-sample speed matters most.

A coda: where custom audio processing goes from here

Two more corners of the platform build on everything this module has covered but push it toward 3D. PannerNode positions a source in simulated 3D space around a listener, and with an HRTF (head-related transfer function) panning model, it filters the signal the way a real human head, ears, and shoulders would filter sound arriving from a given direction — the same convolution intuition from filters and effects, applied to spatial hearing instead of room acoustics. It's the natural next step once you have both a graph of processing nodes and, via AudioWorklet, the ability to write your own — spatial and custom DSP are where the API's current growth is concentrated.

The module, end to end

Eight lessons, one model running through all of them: sound is a graph of nodes evaluated on a dedicated render thread with a merciless deadline (lesson 1). What that thread actually pushes around is an array of samples at a fixed rate (lesson 2), produced or played back by source nodes (lesson 3) whose parameters are driven not by plain assignment but by a schedulable AudioParam timeline ticking against the audio clock (lesson 4). Gain nodes use that same scheduling machinery to shape a note's envelope and to mix multiple sources together (lesson 5). Filters, delay, and convolution reshape frequency content, add repetition, and stamp a signal with a room's acoustic fingerprint (lesson 6). An analyser taps that graph so ordinary main-thread code can read it back as a waveform or, via the FFT, a spectrum (lesson 7). And when none of that native machinery is enough, AudioWorklet hands you the render thread itself, sample by sample, to write DSP of your own (lesson 8). Every one of those pieces is a different answer to the same underlying question the first lesson asked: how do you produce sound, correctly, on a clock that never waits for you?

Where this goes next

This module ends here, but the two threads running through it — real-time deadlines and the graph-as-domain-model — aren't unique to audio. The animation module works through the visual side of the same deadline pressure, the canvas and Three.js modules build their own graphs of GPU state under a similar clock, and WebAssembly is the tool this lesson leaned on for the moment JavaScript itself becomes the bottleneck. The rest of the curriculum keeps pulling on those same two threads from different angles.

Go deeper

  • MDN — AudioWorklet addModule, the worklet global scope, and how it differs from a regular Worker, straight from the reference this lesson's wiring code follows.
  • MDN — AudioWorkletProcessor The process(inputs, outputs, parameters) signature, parameterDescriptors, and the port property this lesson's processor classes are built from.
  • W3C spec — AudioWorklet The authoritative rendering model for worklet processors, including the real-time constraints and the render-quantum call cadence this lesson describes.

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. What does AudioWorkletProcessor's process() method receive as arguments, and how often is it called relative to the render quantum from lesson 1?
  2. Why must process() avoid allocation and blocking work, and what specifically goes wrong if it misses its deadline?
  3. Walk through the three steps of getting a custom processor running: what does addModule do, and what does AudioWorkletNode give you once it resolves?
  4. What problem did ScriptProcessorNode have that AudioWorklet fixes, and why does moving the work to the audio thread fix it?
  5. Describe the two ways a worklet processor communicates with the main thread, and what each is suited for.
  6. Why is a worklet's process() loop a natural place to call into WebAssembly, tying your answer to what makes WASM fast in the module on that topic?
  7. In one or two sentences each, summarize what lessons 1 through 7 established and how AudioWorklet in lesson 8 builds on all of them.