WASM Synth
Overview
Section titled “Overview”WASM Synth (aka Crustacean Cerulean) is a fully-featured polyphonic synthesizer that runs entirely in the browser. The core DSP — oscillators, filters, and envelope generators — is written in Rust, compiled to WebAssembly, and executed inside AudioWorkletProcessor threads for glitch-free, real-time audio. A React + TypeScript frontend provides a DAW-style interface with a mixer, piano roll sequencer, multiple synth instances, and MIDI support.
Live Demo
Section titled “Live Demo”Try it at jadujoel.github.io/rs-wasm-ts-worklet
Features
Section titled “Features”- Rust DSP in AudioWorklet — Oscillator, filter, and ADSR envelope processors run as WASM modules inside dedicated audio threads via the waw-rs framework, keeping the main thread free for UI
- PolyBLEP Anti-Aliased Oscillators — Sine, sawtooth, square, and triangle waveforms with polynomial band-limited step corrections to minimize aliasing artifacts
- Chamberlin State-Variable Filter — A two-pass low-pass filter with automatable cutoff (20 Hz – 20 kHz) and resonance, implemented in pure Rust with no wasm dependencies for easy unit testing
- Up to 8 Stackable Oscillator Layers — Each layer has independent waveform, pitch offset, pan, ADSR envelope, and filter — link toggles allow syncing envelopes/filters across all layers simultaneously
- Wavetable Synthesis — Five built-in wavetable presets (Mellow Saw, E. Piano, Organ, Violin, Pulse 25%) plus drag-and-drop custom wavetable loading from any audio file
- Full Effects Chain — Tempo-syncable stereo delay with ping-pong mode, Juno-60 style BBD chorus (three modes: I, II, I+II), and a dynamics limiter/compressor with transparent and clipper modes
- Arpeggiator — Six patterns (up, down, up-down, down-up, random, as-played), tempo-synced rate divisions, 1–4 octave range, latch mode, and adjustable gate length
- Piano Roll Sequencer — Full MIDI-style sequencer with draw/select/erase tools, marquee selection, copy/paste, arrow-key movement, velocity lane, quantize grid, and pattern presets
- Web MIDI & Keyboard Input — Hardware MIDI controller support with CC mapping (cutoff, resonance, ADSR), sustain pedal, pitch bend — plus a two-octave computer keyboard mapping
- Multi-Synth Mixer — Up to 8 independent synth instances with per-channel volume, pan, mute/solo, and MIDI channel routing in a DAW-style channel-strip layout
- Just Intonation Mode — “Clean Chords” feature retunes intervals to 5-limit just intonation ratios in real time, eliminating tempered beating
- Session Persistence & Sharing — Full state serialized to localStorage with versioned migration; shareable Base64-encoded URLs capture the entire synth configuration
- PWA with Offline Support — Service worker handles COOP/COEP headers for
SharedArrayBuffersupport and precaches all assets for offline use
How It Works
Section titled “How It Works”The Rust DSP code in src/ compiles to a cdylib WASM module via wasm-bindgen. On startup, the TypeScript layer in frontend/initAudio.ts loads the WASM binary, creates an AudioContext at 48 kHz, and registers the worklet processors:
// src/lib.rs — entry point#[wasm_bindgen(js_name = registerContext)]pub async fn register_context() -> AudioContext { set_panic_hook(); let ctx = AudioContext::new().unwrap(); polyfill(&ctx).await; waw::register_all(&ctx).await.unwrap(); ctx}Each voice is a chain of WASM AudioWorkletNodes: Oscillator → Filter → Envelope. The SynthController class manages polyphonic voice allocation, stealing voices when the limit is reached, and routing everything through the effects chain (chorus → delay → limiter → master bus).
The oscillator generates samples using PolyBLEP anti-aliasing:
pub fn generate_sample(waveform: Waveform, phase: f32, dt: f32) -> f32 { match waveform { Waveform::Sawtooth => { let naive = 2.0 * phase - 1.0; naive - poly_blep(phase, dt) } Waveform::Square => { let naive = if phase < 0.5 { 1.0 } else { -1.0 }; let mut out = naive; out += poly_blep(phase, dt); out -= poly_blep((phase + 0.5) % 1.0, dt); out } // ... }}The dev server (index.ts) uses Bun.serve() with cross-origin isolation headers (COOP/COEP) required for SharedArrayBuffer support that the WASM threading layer depends on.
Testing
Section titled “Testing”The project has two layers of tests:
- Unit tests (Bun) — 12 test suites covering arpeggiator logic, MIDI mapping, note utilities, preset validation, state storage migrations, key mapping, and synth type contracts
- E2E tests (Playwright) — Browser tests verify the app loads without console errors, WASM initializes correctly, React mounts into the DOM, and static assets serve with proper MIME types and COOP/COEP headers
- Rust tests — The pure DSP modules (
filter_dsp.rs,waveform.rs,envelope_dsp.rs) include native Rust unit tests for signal correctness — verifying that DC passes through the LPF, high frequencies are attenuated, waveform parameter mapping is accurate, and envelope phases transition correctly
Tech Stack
Section titled “Tech Stack”| DSP Language | Rust (2021 edition) |
| Frontend | React 19, TypeScript |
| WASM Bridge | wasm-bindgen, waw-rs |
| Audio API | Web Audio API, AudioWorklet, PeriodicWave |
| Build | Bun, Cargo, wasm-pack |
| Testing | Bun test, Playwright, Rust #[test] |
| Icons | Lucide React |
| Linting | Biome |
| Deployment | GitHub Pages (PWA) |
Source Code
Section titled “Source Code”The source code is available on the project’s GitHub repository.