WASM SIMD inside an AudioWorklet: a practical guide

How to move hot DSP loops into WebAssembly SIMD without copying buffers or stalling the audio thread.

Published

Audio DSPWebAssembly

Why the audio thread is special

An AudioWorklet's process() method is called for every render quantum of 128 samples. At 48 kHz that is 375 calls per second, and each one has a hard deadline: if it returns late, the listener hears a click. Anything that can pause the thread — memory allocation that triggers garbage collection, a large copy, a lock — is a risk.

So the first rule is not about speed at all. It is about predictability: allocate everything up front, reuse buffers, and keep the per-block work bounded.

Sharing memory instead of copying

A WebAssembly module has a linear memory that JavaScript can see as an ArrayBuffer. Reserve regions of that memory for input and output once, create Float32Array views over them, and on each block copy the worklet's input into the input view, call the WASM function with pointers, and read the output view.

That is one small copy in and one out per channel, instead of marshalling arrays across the boundary. If the module's memory grows, existing views become detached, so either size the memory generously at start or rebuild the views after growth.

Where SIMD helps

WASM SIMD exposes 128-bit vectors, which hold four 32-bit floats. Gain, mixing, simple filters run across channels, and waveshaping with polynomial approximations all map naturally to processing four samples at once.

Recursive filters are harder, because each output depends on the previous one. Those can be vectorised across channels or across parallel filter bands instead of across time.

  • Profile before vectorising — only the hot loops are worth it
  • Keep scalar fallbacks for testing and for comparing output
  • Compare SIMD and scalar output sample-by-sample in tests

Parameter changes

Parameters arrive from the UI thread through the worklet's message port or AudioParams. Jumping a gain from one value to another in a single sample produces audible zipper noise, so ramp the value linearly across the block. The ramp itself is a good SIMD candidate.

Related projects