Web-based Software Defined Radio. Browser-native alternative to SDR++.
Connects to an RTL-SDR dongle via rtl_tcp, streams raw IQ data to the browser, and performs all signal processing (FFT, FM demodulation) client-side via Rust compiled to WebAssembly.
[Machine with SDR dongle] [Browser]
rtl_tcp :1234 WASM DSP (Web Worker)
↓ ├── FFT → spectrum/waterfall
backend (proxy) :8080 ←── WebSocket ──→ └── WFM demod → audio
(raw IQ passthrough)
The backend is a thin TCP→WebSocket proxy with zero per-client DSP cost. All heavy processing runs in the browser.
- Real-time spectrum analyzer with frequency axis and peak hold
- Scrolling waterfall spectrogram
- Wideband FM demodulation with browser audio playback
- All DSP in browser via Rust→WASM (FFT, FIR filters, FM discriminator, de-emphasis)
- Frequency tuning and gain control (manual + AGC)
- Keyboard shortcuts (arrow keys for tuning, Space for play/stop)
- Auto-reconnection with exponential backoff
- Multi-client support (proxy scales to many clients)
- RTL-SDR dongle +
rtl_tcp(from librtlsdr) - Rust toolchain (1.75+) with
cargoavailable on yourPATH - wasm-pack (
cargo install wasm-pack) - Node.js (20+)
# Terminal 1: start rtl_tcp
rtl_tcp -a 127.0.0.1
# Terminal 2: start backend (proxy)
cd backend
cargo run --release -- --frequency 81300000 # J-WAVE 81.3 MHz
# Terminal 3: build WASM + start frontend
wasm-pack build --target web --out-dir ../frontend/src/wasm wasm-dsp
cd frontend
npm install
npm run dev --turbopack
# Open http://localhost:3000| Flag | Default | Description |
|---|---|---|
--rtl-host |
127.0.0.1 | rtl_tcp server host |
--rtl-port |
1234 | rtl_tcp server port |
--ws-port |
8080 | WebSocket server port |
--frequency |
90100000 | Initial frequency (Hz) |
--sample-rate |
2048000 | Sample rate (Hz) |
Thin TCP→WebSocket bridge. Connects to rtl_tcp, streams raw IQ data to browsers, relays control commands back. No DSP.
Key dependencies: tokio, tokio-tungstenite, clap, tracing
All DSP runs in the browser via WebAssembly, inside a Web Worker (off the main thread):
u8 IQ @ 2.048 Msps (from WebSocket)
→ [FFT branch] 2048-pt FFT → dB magnitude → spectrum/waterfall (20 fps)
→ [Audio branch] lowpass + decimate ÷8 → 256 kHz
→ atan2 FM discriminator
→ lowpass + decimate ÷5 → ~48 kHz
→ de-emphasis 50μs (Japan)
→ f32 PCM → Web Audio API
~200 lines of custom DSP. Only external DSP dep: rustfft + num-complex. WASM binary: ~217 KB.
| Component | Role |
|---|---|
SpectrumDisplay |
Canvas line graph with frequency axis, peak hold, gradient fill |
WaterfallDisplay |
Scrolling spectrogram with color LUT (blue→red) |
FrequencyControl |
MHz input + step buttons (±100kHz, ±1MHz) |
GainControl |
Gain slider + AGC toggle |
useWasmDsp |
Web Worker lifecycle, routes IQ→FFT/audio |
useAudioPlayback |
Web Audio API ring buffer, ScriptProcessorNode |
useSDRConnection |
WebSocket lifecycle, reconnection, frame parsing |
Single connection on ws://localhost:8080, multiplexed:
- Binary frames: 1-byte type tag + payload
0x01Raw IQ: uint8 interleaved IQ pairs0x03Status: JSON (tuner info, connection state)
- Text frames: JSON control commands (
set_frequency,set_gain,set_agc)
# Build and test everything
cargo test # Rust workspace tests (backend + wasm-dsp)
cargo fmt --all # format Rust code
cargo clippy --all-targets --all-features -- -D warnings
# Backend
cd backend
RUST_LOG=debug cargo run --release
# WASM DSP
wasm-pack build --target web --out-dir ../frontend/src/wasm wasm-dsp
# Frontend
cd frontend
npm install
npm run dev --turbopack # dev server
npm run build # production build
npm run lint # lintIf cargo is not found, make sure your Rust toolchain is installed and loaded into your shell first.
MIT