Skip to content

WASM Synth

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.

Try it at jadujoel.github.io/rs-wasm-ts-worklet

  • 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 SharedArrayBuffer support and precaches all assets for offline use

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:

src/waveform.rs
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.

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
DSP LanguageRust (2021 edition)
FrontendReact 19, TypeScript
WASM Bridgewasm-bindgen, waw-rs
Audio APIWeb Audio API, AudioWorklet, PeriodicWave
BuildBun, Cargo, wasm-pack
TestingBun test, Playwright, Rust #[test]
IconsLucide React
LintingBiome
DeploymentGitHub Pages (PWA)

The source code is available on the project’s GitHub repository.