Technical Documentation

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

DSP Language
C++ -> WASM

Signal storage, modulation kernels, carrier synthesis, and FFT exports are compiled with Emscripten.

Execution Model
100% Browser

Computation and memory live on the client: no backend simulation service.

Baseline Rate
48 kHz

Frontend raises the rate further when Nyquist or oversampling constraints exceed the floor.

Runtime stack
  • App shell: Next.js App Router + React client components.
  • DSP load path: dynamic import of /wasm/dsp.js plus browser instantiation of dsp.wasm.
  • Bridge: dspClient.ts wraps exported C ABI with Emscripten cwrap.
  • Data boundary: WASM signal buffers are copied into JavaScript Float32Array snapshots for plotting, playback, and recorded-message reuse.
High-level flow
UI controls -> SignalWorkspace state
           -> dspClient bridge
           -> C++ signal allocation and modulation
           -> signal snapshots copied back to JS
           -> waveform panel, FFT panel, and audio preview

2. Browser Runtime

Execution model

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.

Time base and frequency verification

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.

WASM bridge

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"
]);
Audio capture path

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.

Clip Duration
1 s - 10 s

UI-bound recording window.

Clip Bandwidth
5 kHz LPF

Applied before message reuse.

3. Signal Pipeline

Generation path

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(...)
FFT processing

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.

How much data is processed

Signal length is computed as:

sampleCount = max(
  1024,
  fftSize,
  ceil(sampleRate * requestedWindowSeconds)
)
Default FFT
8192

Dominates the default sample count at the 48 kHz floor and 0.125 s window.

Default JS Sample Payload
~192 KB

Six 8192-sample Float32Array snapshots per update: three time-domain plus three spectra.

Max Recorded Clip Buffer
~480 KB

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

Build pipeline

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.js
Project layout
frontend/
  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