This is a technical post about how Morph's Soundbend — a full digital audio workstation including 5 native instruments, a Mixer with FX chains and aux sends, 30+ effects (including 5 Airwindows Tier-1 ports and a true-peak limiter), a MIDI effects rack, a WAM v2 plugin host, and a Yjs CRDT collaboration layer — runs entirely in a browser tab.
The practical question is: is this actually competitive with the desktop DAWs? The honest answer: for most producer workflows, yes. For deep third-party device ecosystems, not yet. Open Soundbend to hear the product itself. This post is the engineering.
The stack
- React + Vite frontend
- Web Audio API for the audio graph
- AudioWorkletProcessor for custom DSP (off-main-thread)
- WebAssembly for performance-critical DSP (custom WASM builds)
- MessagePort + ring buffers for UI ↔ worklet communication
- Yjs for CRDT collaboration
- IndexedDB for project persistence + sample storage
- WebMIDI for hardware controller input
- OfflineAudioContext for bounce/export
1. The audio graph
Every track in Soundbend routes through a chain: instrument → track FX chain → aux send → Mixer strip → master bus → limiter → destination. Each arrow is a Web Audio AudioNode connection.
Web Audio gives us AudioContext, GainNode, BiquadFilterNode, ConvolverNode, AnalyserNode, and friends. These are optimized by browser engines (V8/SpiderMonkey/JSC all deeply invested). For standard effects — EQ, gain automation, basic filtering — we use native nodes. They're fast and well-tested.
For custom effects — Airwindows ports, our saturation, our true-peak limiter — we drop into AudioWorkletProcessor, which runs on the audio thread, guaranteed real-time priority, and is the correct architectural place for nonstandard DSP.
class PurestDriveProcessor extends AudioWorkletProcessor {
process(inputs, outputs) {
const input = inputs[0];
const output = outputs[0];
// Airwindows PurestDrive port — ~30 LOC of saturation math
for (let ch = 0; ch < input.length; ch++) {
for (let i = 0; i < input[ch].length; i++) {
const s = input[ch][i];
// [...tape-saturation math...]
output[ch][i] = processed;
}
}
return true;
}
}
registerProcessor('purest-drive', PurestDriveProcessor);
Worklets ship in public/audio-worklets/*.js and load via audioContext.audioWorklet.addModule(path). Once loaded, any part of the graph can instantiate an AudioWorkletNode for that processor. Processing runs off the main thread — UI animations don't starve audio.
2. The 5 Airwindows Tier-1 ports
Chris Johnson's Airwindows is 2,500+ audio plugins, MIT-licensed, legendarily tasteful DSP. We ported 5 to browser AudioWorklets for Soundbend's Wave 12 ship:
- Pressure6 — transparent compression that breathes
- ToTape9 — analog tape saturation modeling
- PurestDrive — minimum-phase saturation without harsh highs
- CreamCoat — warmth + harmonic exciter
- NotJustAnotherDither — TPDF dither for bit-depth reduction (mastering)
Each port is ~30-80 LOC of math in an AudioWorkletProcessor. The ports don't match Airwindows bit-for-bit (different FMA/denormal handling between C++ and JS numeric engines) but they match sonically. Null-tests keep the outputs ≤ -80 dBFS residual against goldens, so DSP changes are caught.
3. The true-peak limiter
Modern streaming platforms (Spotify, Apple Music, YouTube) enforce true-peak loudness normalization per ITU-R BS.1770-5. Sample peaks and inter-sample peaks are different things; a signal can peak at -1 dBFS sample-wise but exceed 0 dBFS true-peak after D/A reconstruction. A non-true-peak limiter will clip in the DAC even though your DAW shows it clean.
Our limiter implements the full ITU spec: 4x oversampling, inter-sample-peak detection, lookahead, soft-knee release. It ships in AudioWorklet form at public/audio-worklets/true-peak-limiter-processor.js.
4. Off-main-thread communication
The audio graph runs on the audio thread. The UI runs on the main thread. Parameter changes (turn a knob, fade a track) need to propagate from UI → audio without glitching the audio.
Web Audio's AudioParam handles this natively for standard params (GainNode gain, BiquadFilterNode frequency). For custom worklet params, we use MessagePort with structured cloning for UI messages, and SharedArrayBuffer-backed ring buffers for high-rate automation data.
SharedArrayBuffer requires Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp headers. These are set in public/_headers for the /soundbend + /pulse routes. The Tape Delay uses a ring buffer at 96kHz to avoid message-port latency.
5. The 5 native instruments
Soundbend ships 5 instruments, all custom:
FM Synth — 6-operator FM synthesis modeled on DX7 / Operator. Algorithm visualizer + full modulation matrix. ~800 LOC JS for the UI, ~300 LOC worklet for the voice allocator.
Wavetable Synth — morph-between-tables synthesis. Spectral warping + unison + stereo detune. 30+ factory wavetables. Uses ConvolverNode for wavetable crossfade at audio rate.
Analog Subtractive — 2-oscillator polysynth with Cytomic SVF filters. Cytomic is the state-variable filter topology that sounds right; naive SVF has aliasing issues above fc > SR/6. The Cytomic port handles this via zero-delay feedback math.
DrumRack + Sampler — pad-based sample playback with per-pad pitch/filter/amp envelopes. Samples stored in IndexedDB for fast recall. 5 genre-specific factory kits (boombap, trap, house, dnb, afrobeat).
SeedSynth + Geist (in the legacy AudioStudioEngine — now merged into Soundbend as of Wave 14) — a genetic patch-generator (Synplant 2-class) and a Geist-style drum machine. These are the proprietary IP that made the consolidation doctrine a big deal — we had to preserve bit-identical behavior through a 30k-line merge. 100-seed null-test passed post-merge.
6. The Mixer + aux sends
A proper Mixer — not just per-track faders but FX inserts per track, pre/post-fader aux sends, master bus — is what separates a DAW from a sequencer. Soundbend's Mixer class is ~655 LOC (src/audio/mixer.js). Each track is an AudioNode graph with insert slots, send tap points, and metering.
The tricky part is pre-fader vs post-fader aux sends. Pre-fader sends to a reverb bus regardless of track volume (classic use: parallel reverb that survives a track fade). Post-fader scales with track volume. Both ship.
7. MIDI + WAM plugin host
WebMIDI is mature — controller input from any USB-MIDI keyboard Just Works. Our MIDI rack (Arpeggiator, Scale, Chord) sits between the controller and the instrument, transforming note streams.
For plugins, we host Web Audio Modules v2 (WAM) — browser-native plugins. Smaller ecosystem than VST (25-year head start) but growing. Loading a WAM is getWamHost().load(wamUri) which returns an AudioNode you can insert into any chain.
WAMs require per-route CSP because they load arbitrary user-provided JS. Our _headers scopes CSP tightly for /soundbend with a script-src allow-list for well-known WAM registries.
8. OfflineAudioContext for bounce
Export runs via OfflineAudioContext — render faster than real-time (or slower, depending on chain complexity) to a static AudioBuffer, then encode to WAV via our in-browser encoder.
Catch: not all nodes work in OfflineAudioContext. MediaStreamAudioSourceNode doesn't (can't record live mic offline, obviously). Certain worklets need careful state-reset between online → offline transitions. We ran into this repeatedly and now Mixer.toJSON() + Mixer.fromJSON(offlineCtx) is the canonical pattern: serialize the mixer state, rebuild a parallel mixer on the offline context, render.
9. Collaboration via Yjs
Yjs is a CRDT library that handles multi-user collaborative editing with automatic conflict resolution. Our SoundbendSession class wraps Yjs with per-track document structure (Y.Map for track metadata, Y.Array for pattern steps, Y.Text for lyrics/notes).
Two-to-four producers can edit the same project simultaneously with sample-accurate track sync. This is harder than text-doc collab because audio data is orders of magnitude larger — we share metadata + patterns via Yjs, not raw audio. Raw samples stay local (IndexedDB per user).
10. The performance budget
Running a full DAW in a browser with 60fps UI + real-time audio + SharedArrayBuffer + WASM + multiple worklets requires tight budgets. Some numbers:
- Audio thread: ~2ms / 128-sample buffer at 48kHz (sub-frame)
- Main thread: 16.6ms / 60fps frame; audio handling must stay <1ms per frame
- Worklet setup: one-time cost ~10-50ms per worklet load (ship 15+ worklets — we batch)
- SharedArrayBuffer allocation: 4KB ring buffers per dynamic worklet param
- Soundbend chunk size (post-consolidation): ~496 KB raw, ~120 KB gzip
- Full project load (cold): ~800ms on mid-range laptop
- Export render: ~0.3x to 2x realtime depending on effect chain complexity
11. Null-test harness
DSP regressions are silent — audio sounds different in subtle ways that code review can't catch. Soundbend ships with a Puppeteer-driven null-test harness in tests/audio-null/.
For each effect × preset × test signal, the harness renders via OfflineAudioContext inside a headless Chromium, null-tests the resulting WAV against a committed golden, and fails the build if residual peak exceeds -60 dBFS.
Wave 14F added 100 SeedSynth seed baselines and 32 Geist drum-pattern goldens. When we merged Soundbend into AudioStudio (Wave 14B-G), the null-test caught anything that would have silently broken the proprietary instruments. 96 of 100 SeedSynth seeds passed bit-identical. The 4 fails were pre-existing Chromium Web Audio quirks (high-Q filter resonance drift between OfflineAudioContext instances) — not merge regressions, and not fixable from JS.
DSP regressions are silent. Code review can't hear them. The null-test harness can — that's why it's CI-blocking.
12. Why browser + client-side was the right bet
Native DAWs have 30-year head starts. Beating them feature-for-feature is a long road. But pricing — that was winnable immediately by making different architectural bets.
WebAssembly crossed the performance threshold in 2022-2024. Web Audio AudioWorklets shipped widely around the same time. SharedArrayBuffer + COOP/COEP stabilized. Chromium + Firefox + Safari all support the needed APIs at adequate performance.
The pricing implication: if audio processing runs on the user's device, we don't pay for it. Our marginal cost per Pro subscriber is near-zero.
Read the manifesto for the economics. For the DSP, that's most of the story.
Try it
Soundbend is at morph.cool/soundbend. Free tier works without signup. WebMIDI keyboard welcome, not required.
— the Morph team
April 22, 2026