An interactive, in-browser demonstration of Single Side Band (SSB) modulation — how it differs from full AM, why an envelope (AM) detector can't recover it, and how a real SSB receiver gets the signal back by re-injecting the carrier.
▶ View online: begoon.github.io/ssb
Starting from a message — either a 1 kHz test tone or your microphone — the app builds and plots:
- The message waveform.
- Full AM (carrier + both sidebands):
(1 + m·s)·cos(ωc·t), with the envelope drawn on top. - SSB (upper sideband only — carrier and lower sideband suppressed), via the
phasing/Hilbert method:
s·cos(ωc·t) − ŝ·sin(ωc·t). - Spectra of AM vs SSB, zoomed around the carrier so the suppressed carrier and lower sideband are obvious.
- A recovery comparison of the original message against two demodulation attempts.
- AM decoder attempt (envelope detection) — rectify + low-pass.
- For a tone the SSB envelope is constant → silence. (
√(cos² + sin²) = 1.) - For voice you get the analytic (Hilbert) envelope of your speech: distorted, wrong pitch, partly recognizable — but not the real signal. This is fundamental, not a tuning artifact: the amplitude envelope of speech is inherently informative.
- For a tone the SSB envelope is constant → silence. (
- SSB receiver (product / coherent detection) — re-inject the carrier and low-pass:
LPF{ ssb · 2·cos(ωc·t) }→ clean recovery.- The BFO offset slider simulates a mistuned re-injected carrier: every recovered frequency shifts by that many Hz (the "Donald Duck" effect). It's why real SSB radios have a clarifier/RIT knob.
A real SSB voice carrier is in the MHz range (HF/CB), but a browser can't sample or play that. So each domain uses the highest frequency it can actually represent, keeping the carrier-to-message ratio intact:
| Domain | Carrier | Why |
|---|---|---|
| Tone — visual plots | 1 MHz (default) | synthetic math, oversampled, no real-time limit |
| Mic — spectrum | 27 MHz (default) | exact frequency translation of the voice band (CB-SSB) |
| Mic — time view & all audio | scaled (~8 kHz / 5 kHz) | real audio is 48 kHz → must stay under the 24 kHz Nyquist |
The mic spectrum is shown at a true RF carrier because SSB is just a frequency shift of the voice band — so the displayed axis is translated up to the carrier exactly, while the time waveform and playback necessarily stay in a scaled, audible/representable domain.
Requires Bun. Recipes via just:
just dev # dev server with hot reload at http://localhost:3000
just test # run the DSP unit tests
just build # bundle into a single self-contained docs/index.html
just open # build + open the single-file page (no server)Without just:
bun install
bun index.ts # dev server
bun test # tests
bun run build.ts # produce docs/index.htmlThe microphone needs a secure origin, so use http://localhost:3000 (dev server) or the
built file — getUserMedia won't prompt from a bare file:// in some browsers.
just build writes a fully self-contained docs/index.html (all JS inlined, no external
requests). Point GitHub Pages at the docs/ folder to publish.
| File | Role |
|---|---|
dsp.js |
FFT, Hilbert transform, AM/SSB synthesis, envelope & product detectors, filters |
plot.js |
Canvas waveform / spectrum / multi-trace plotting (theme-aware) |
audio.js |
Web Audio playback, mic capture, building the demodulation buffers |
frontend.js |
UI wiring: controls → DSP → plots + audio |
index.html |
Layout, styling, light/dark theme |
index.ts |
Bun.serve() dev server |
build.ts |
Bundles + inlines everything into docs/index.html |
dsp.test.ts |
Unit tests for the DSP core |
bun test covers the DSP core: the Hilbert transform (90° shift), SSB constant amplitude
for a tone, carrier/lower-sideband suppression, envelope detection (AM recovers, SSB is
silent), and product detection (re-injected carrier recovers the message; a detuned carrier
does not).
MIT © 2026 Alexander Demin
