Real-time audio recorder with delay and reverb — DSP core in C++, compiled to WebAssembly via Emscripten, UI in Web Audio API.
Built as a browser-deployable demonstration of the same DSP topology used in EchoPsychFX (JUCE/C++ plugin).
Mic (MediaStream)
└─► ScriptProcessorNode (JS glue)
├─► copy Float32 samples into WASM HEAPF32 input buffer
├─► EchoChamber_process() [C++ / WASM]
│ ├─ DelayLine — ring buffer + linear interpolation + feedback loop
│ └─ ReverbEngine — Schroeder network (4× comb + 2× all-pass + pre-delay)
└─► copy WASM output buffer back to Web Audio output
All DSP runs in echo_chamber.cpp. JavaScript handles only:
- Audio I/O (Web Audio API, MediaRecorder)
- WASM memory bridge (HEAPF32 reads/writes)
- UI and parameter forwarding
A power-of-2 RingBuffer with linear interpolation for sub-sample accuracy. Feedback is clamped to 0.95 to prevent energy divergence. Parameters are smoothed with a one-pole filter (coefficient ≈ 0.995) to eliminate zipper noise on real-time parameter changes.
Same topology as the delay line in EchoPsychFX. The main difference in the JUCE version: the buffer is allocated in prepareToPlay() and the feedback path runs through a dsp::DelayLine<float> with identical ring arithmetic.
Schroeder / Moorer / Gardner architecture:
- Pre-delay —
RingBuffer, up to 500 ms. Encodes perceived room size (Gardner 1992: pre-delay < 20 ms → small room, > 50 ms → large hall). - 4× parallel comb filters — delay lengths are mutually prime (1557, 1617, 1491, 1422 samples at 44100 Hz) to avoid beating artefacts. Feedback gain is derived from target RT60:
g = 10 ^ (-3 × delayTime / RT60) - 2× series all-pass diffusors — Schroeder all-pass (|H(f)| = 1) breaks up spectral coloration from the comb bank. Gain = 0.5 (Schroeder's recommendation).
This is the same topology as Freeverb (Jezar at Dreampoint, 2000), which is itself the canonical open-source implementation of the Schroeder model.
One-pole IIR smoother applied per-sample to every parameter. Without this, fast slider movement causes audible zipper noise (step discontinuities at block boundaries). The same pattern is used in every JUCE AudioProcessor that handles real-time parameter automation.
# Install Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh # Windows: emsdk_env.batVerify: emcc --version should print a version string.
cd echo_chamber
chmod +x build.sh
./build.shOutput: dist/echo_chamber.js (WASM binary inlined as base64 via SINGLE_FILE=1).
# Serve locally — required because WASM needs HTTP (not file://)
python3 -m http.server 8080
# open http://localhost:8080/echo-chamber.htmlOr drop echo-chamber.html and dist/echo_chamber.js (renamed to echo_chamber.js) into any web server directory.
echo_chamber/
├── echo_chamber.cpp # C++ DSP core (delay, reverb, WASM exports)
├── echo-chamber.html # Browser UI + Web Audio / WASM glue
├── build.sh # Emscripten build script
├── dist/
│ └── echo_chamber.js # Generated — Emscripten output (WASM + glue, single file)
└── README.md
The DelayLine and ReverbEngine classes here are a simplified version of the processing chain in EchoPsychFX (JUCE VST3/AU). The main differences:
| Echo Chamber (WASM) | EchoPsychFX (JUCE) | |
|---|---|---|
| Delay | Ring buffer, linear interp | juce::dsp::DelayLine, Lagrange interp |
| Reverb | Algorithmic Schroeder | Algorithmic + convolution option |
| Parameter smoothing | Manual one-pole | juce::SmoothedValue |
| Threading | Main thread (ScriptProcessor) | Dedicated audio thread |
| Format | WebAssembly | VST3 / AU / CLAP |
The C++ DSP classes (RingBuffer, CombFilter, AllpassFilter, Smoother) are designed to be portable — they compile cleanly for JUCE, Wwise, or Emscripten targets with no platform-specific dependencies.
MIT — Aliz Pasztor · alizpasztor.com · Trailblaiz