Audio DSP
GK Heart Engine
A browser audio engine built on AudioWorklet and WASM SIMD kernels, sharing its preset format with JUCE plugins via JSON.
Published · Updated
- AudioWorklet
- WebAudio
- WASM SIMD
- TypeScript
- C++
- JUCE
What it is
GK Heart Engine is the audio core behind dsp.gkheart.ca. It runs entirely in the browser: audio is processed inside an AudioWorklet on the audio rendering thread, and the heavy inner loops are compiled to WebAssembly with SIMD instructions so several samples are processed per instruction.
The engine describes its signal chain and parameters as JSON. That same description can be read by a native JUCE plugin, so a sound designed in the browser and a sound loaded in a desktop VST come from one shared source of truth instead of two hand-maintained copies.
The problem it solves
Browser audio usually means choosing between convenience and control. The built-in WebAudio nodes are easy to wire up but fixed in behaviour; custom DSP in plain JavaScript is flexible but competes with garbage collection and the main thread. Musicians notice both: odd tone, or clicks and dropouts.
Moving custom processing into an AudioWorklet removes the main-thread problem, and moving the math into WASM SIMD removes most of the per-sample cost. The result is custom DSP that behaves predictably in a tab.
How it works
The UI thread owns the patch: a JSON document listing modules (gain stages, filters, waveshapers, cabinet-style filtering, delays) and their parameters. When the patch changes, the UI posts a compact message to the worklet rather than rebuilding the audio graph.
Inside the worklet, each 128-sample render quantum is passed through the module chain. Modules with expensive inner loops call into a WASM module that works on a shared linear-memory buffer, so audio data is not copied back and forth between JavaScript and WebAssembly on every block.
Parameter changes are smoothed across the block to avoid zipper noise, and nothing in the render path allocates memory, which keeps the garbage collector away from the audio thread.
Lessons from building it
The hardest bugs were not in the math but at the boundaries: buffer ownership between JS and WASM, message timing between the UI and the worklet, and making sure that a JSON patch means exactly the same thing in the browser and in C++.
Writing the JSON schema down first, and treating it as a contract that both runtimes are tested against, removed a whole class of 'it sounds different in the plugin' problems.
- No allocations in the render callback
- One JSON schema shared by browser and native builds
- SIMD only where profiling showed it mattered