Browser-side DSP architecture
Next.js UI, TypeScript control layer, C++ DSP core, Emscripten WebAssembly target. Signal generation, FFTs, microphone preprocessing, playback, and plotting execute in the user's browser. No server-side DSP path exists.
1. System Overview
Signal storage, modulation kernels, carrier synthesis, and FFT exports are compiled with Emscripten.
Computation and memory live on the client: no backend simulation service.
Frontend raises the rate further when Nyquist or oversampling constraints exceed the floor.
- App shell: Next.js App Router + React client components.
- DSP load path: dynamic import of
/wasm/dsp.jsplus browser instantiation ofdsp.wasm. - Bridge:
dspClient.tswraps exported C ABI with Emscriptencwrap. - Data boundary: WASM signal buffers are copied into JavaScript
Float32Arraysnapshots for plotting, playback, and recorded-message reuse.
UI controls -> SignalWorkspace state
-> dspClient bridge
-> C++ signal allocation and modulation
-> signal snapshots copied back to JS
-> waveform panel, FFT panel, and audio preview2. Browser Runtime
Parameter edits trigger local recomputation in SignalWorkspace.tsx. The frontend derives sampleRate, sampleCount, FFT resolution, and plot bounds, calls the WASM kernel, reads back six signal snapshots in the typical case, and renders them directly. Microphone capture, decode, low-pass filtering, normalization, and looped playback also stay client-side.
The app does not need a wall-clock timing source to define frequency. Generated signals are discrete-time buffers indexed by sample number n with declared sample rate f_s, so the implied time base is t[n] = n / f_s. Carrier and message samples are synthesized on that uniform grid, so equal spacing is part of the signal definition, not something inferred from browser scheduling. Frequency checks follow directly: f = f_s / N_period in the time domain and Δf = f_s / N_fft for FFT bin spacing. Recorded audio follows the same assumption after browser decode and resampling: once it becomes an AudioBuffer, it is treated as fixed-rate PCM on a uniform grid.
frontend/app/lib/dspClient.ts lazily imports the generated module, binds exported C symbols, and exposes a typed API for allocation, destruction, per-sample read/write, additive message construction, carrier generation, AM, DSB-SC, SSB, FM, PM, and FFT magnitude generation.
const amModulate = wasmModule.cwrap("dsp_am_modulate", "number", [
"number", "number", "number", "number", "number"
]);Capture uses MediaRecorder. The blob is decoded by AudioContext, resampled and filtered in an OfflineAudioContext, mixed to mono, peak-normalized, and stored as a Float32Array. Recorded clips are then rendered into WASM signal buffers as looping message sources.
UI-bound recording window.
Applied before message reuse.
3. Signal Pipeline
Each recomputation produces three primary signals: message, carrier, modulated. The frontend allocates message and carrier buffers in WASM, fills the message from additive tones or recorded audio, synthesizes the carrier, then calls the selected modulation kernel to allocate the output buffer.
messageSignalId = createSignal(length, sampleRate)
carrierSignalId = createSignal(length, sampleRate)
populateMessageSignal(...)
generateCarrier(...)
modulatedSignalId = dsp_*_modulate(...)After time-domain generation, the frontend requests three more WASM signals: magnitude spectra for message, carrier, and modulated outputs. Default FFT size is 8192; supported sizes are 1024, 2048, 4096, 8192, and 16384. The frontend then applies center/span clipping and y-scale transforms for the visible plot.
Signal length is computed as:
sampleCount = max(
1024,
fftSize,
ceil(sampleRate * requestedWindowSeconds)
)Dominates the default sample count at the 48 kHz floor and 0.125 s window.
Six 8192-sample Float32Array snapshots per update: three time-domain plus three spectra.
10 s at 12 kHz is about 120,000 float samples before extra WASM copies and UI state overhead.
These values describe JavaScript-side typed arrays only. Matching buffers also exist inside WASM, so active recomputation uses a larger total in-browser working set.
4. Build and Operations
Emscripten compiles the C++ sources through wasm/Makefile and emits frontend/public/wasm/dsp.js plus frontend/public/wasm/dsp.wasm. Key flags: MODULARIZE, EXPORT_ES6, ENVIRONMENT=web, and ALLOW_MEMORY_GROWTH.
emcc src/*.cpp -O3 --no-entry -s WASM=1 -s MODULARIZE=1 -s EXPORT_ES6=1 -s ENVIRONMENT=web -s ALLOW_MEMORY_GROWTH=1 -o ../frontend/public/wasm/dsp.jsfrontend/
app/
components/workspace/ React UI, plots, controls, routing shell
components/learn/ theory-oriented reference page
components/documentation/ implementation reference page
lib/dspClient.ts JS <-> WASM bridge
lib/audioRecording.ts microphone capture and preprocessing
wasm/
src/dsp_api.cpp exported C ABI shim
src/modulation.cpp modulation implementations
src/signal.cpp signal storage and synthesis primitives
src/fft.cpp FFT magnitude generation
include/ shared headers