Evaluated 7 approaches for multi-channel CB radio monitoring on Raspberry Pi 400. Top candidates: RTLSDR-Airband (existing software, quick setup) and custom C with librtlsdr + liquid-dsp (~950 LOC, meets all requirements exactly). Ruled out SDRTrunk, SDRAngel, GNU Radio, rtl_fm, and Go-based approaches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
Research: SDR Scanner & Recorder — Software Options
Compiled 2026-03-17 Status: Complete — ready for architecture decision
Executive Summary
Five approaches were evaluated for monitoring 3-4 CB radio frequencies (27 MHz band, NFM, EU region) simultaneously with an RTL-SDR dongle on a headless Raspberry Pi 400. The top two candidates are:
- RTLSDR-Airband — existing production software that does almost exactly what we need, with minimal setup. Best for validating the RF setup quickly.
- Custom C with librtlsdr + liquid-dsp — lightweight, fully tailored to requirements, ~800-1200 lines of C. Best for precise control over hang time, WAV output, and YAML config.
The recommended path is: start with RTLSDR-Airband to validate the RF chain, then build custom if its limitations (MP3 output, limited hang time control) are blockers.
Options Evaluated
| Option | Simultaneous Channels | Squelch | WAV Output | Headless Pi 400 | Config File | Complexity |
|---|---|---|---|---|---|---|
| RTLSDR-Airband | Yes | Yes (auto) | No (MP3) | Yes | Yes (libconfig) | Install + configure |
| Custom C (librtlsdr + liquid-dsp) | Yes | Yes | Yes | Yes | Yes (YAML) | ~800-1200 LOC |
| GNU Radio + Python | Yes | Yes | Partial | Yes | Custom wrapper | Medium-High |
| SDRAngel (server) | Yes | Partial | Yes | Yes | REST API only | High |
| SDRTrunk | Yes | Yes | Yes | Marginal | XML + GUI | Over-engineered |
| rtl_fm | No (sequential) | Yes | Via pipe | Yes | CLI flags | Low |
1. RTLSDR-Airband — Best Existing Software
Repository: github.com/rtl-airband/RTLSDR-Airband — 902 stars, GPL-2.0, actively maintained (v5.1.1, March 2025)
What it does: FFT-based multi-channel AM/NFM demodulator designed for narrowband radio monitoring. Splits a wideband IQ capture into sub-channels, demodulates each independently, and records to files with squelch gating.
Fit for Requirements
| Requirement | Status | Notes |
|---|---|---|
| FR-001: Simultaneous monitoring | Yes | Up to 8 channels per dongle in multichannel mode |
| FR-002: NFM demodulation | Yes | Build with -DNFM=ON |
| FR-003: Squelch-based detection | Yes | Auto-squelch (noise floor + ~10 dB) or manual threshold |
| FR-004: WAV recording | No | Outputs MP3. WAV would require source modification or post-processing with ffmpeg |
| FR-005: Hang time / conversation grouping | Partial | split_on_transmission = true splits per-transmission, but hang time granularity is limited |
| FR-006: YAML configuration | No | Uses libconfig format (similar structure, different syntax) |
| FR-007: Console logging | Yes | Logs to stdout/syslog |
Strengths
- Production-grade and battle-tested — used widely for airband monitoring
- Extremely low CPU usage — ~15% on a Pi 3 for one dongle. 3-4 channels on Pi 400 would be trivial
- FFT-based channelizer — computational cost is nearly constant regardless of channel count
- ARM NEON optimized — SIMD on ARMv7+, GPU-accelerated FFT on Pi 2/3
- Simple config file — declarative channel definitions with per-channel squelch and output settings
- Immediate results — install, configure, run
Limitations
- MP3 output only — no native WAV support. MP3 encoding adds some CPU overhead but is negligible for 3-4 channels
- Designed for VHF airband — 27 MHz is outside its typical use case. Should work since it just processes whatever the RTL-SDR captures, but needs testing
- libconfig syntax — not YAML, but functionally equivalent
- Limited hang time control —
split_on_transmissionmode creates separate files per transmission; configurable silence duration exists but may not match the exact "keep recording for N seconds" behavior in FR-005
Example Configuration for CB
devices: (
{
type = "rtlsdr";
index = 0;
gain = 28;
centerfreq = 27185000;
mode = "multichannel";
channels: (
{
freq = 27065000;
modulation = "nfm";
outputs: ({
type = "file";
directory = "/home/pi/recordings";
filename_template = "CB_27065";
split_on_transmission = true;
include_freq = true;
});
},
{
freq = 27185000;
modulation = "nfm";
outputs: ({
type = "file";
directory = "/home/pi/recordings";
filename_template = "CB_27185";
split_on_transmission = true;
include_freq = true;
});
}
// ... more channels ...
);
}
);
Verdict
Best starting point. Gets you from zero to working recordings with minimal effort. Use it to validate the RF setup (antenna, gain, squelch levels, 27 MHz reception quality) before committing to a custom build. If MP3 output and limited hang time control are acceptable, this may be the final solution.
2. Custom C with librtlsdr + liquid-dsp — Best Custom Build
A lightweight purpose-built application that does exactly what the requirements specify and nothing more.
Architecture
┌──────────────────────────────────────────────────────┐
│ RTL-SDR Dongle (centered on 27.185 MHz, 960 kHz) │
└──────────────────────┬───────────────────────────────┘
│ IQ stream (uint8 pairs)
▼
┌──────────────────────────────────────────────────────┐
│ Thread 1: rtlsdr_read_async() → ring buffer │
└──────────────────────┬───────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Channel 1│ │ Channel 2│ │ Channel N│ Thread 2: DSP processing
│ │ │ │ │ │
│ NCO mix │ │ NCO mix │ │ NCO mix │ nco_crcf_mix_down()
│ FIR+dec │ │ FIR+dec │ │ FIR+dec │ firfilt_crcf + decimate
│ FM demod │ │ FM demod │ │ FM demod │ freqdem_demodulate_block()
│ Squelch │ │ Squelch │ │ Squelch │ agc_crcf (6-state machine)
│ Hang time│ │ Hang time│ │ Hang time│ Custom timer logic
│ WAV write│ │ WAV write│ │ WAV write│ PCM file I/O
└──────────┘ └──────────┘ └──────────┘
Fit for Requirements
| Requirement | Status | Notes |
|---|---|---|
| FR-001: Simultaneous monitoring | Yes | Parallel DSP pipelines from single IQ stream |
| FR-002: NFM demodulation | Yes | liquid-dsp freqdem |
| FR-003: Squelch-based detection | Yes | liquid-dsp agc_crcf squelch with 6-state machine |
| FR-004: WAV recording | Yes | Direct PCM WAV write |
| FR-005: Hang time | Yes | Custom timer on top of squelch state machine |
| FR-006: YAML configuration | Yes | Via libyaml or similar lightweight parser |
| FR-007: Console logging | Yes | printf/fprintf to stdout |
Key Libraries
librtlsdr:
- Async read API delivers IQ data via callbacks in 256 KB buffers
- IQ format: 8-bit unsigned interleaved pairs (I, Q, I, Q...)
- Sample rates: 225-300 kHz or 900 kHz–3.2 MHz
- 27 MHz is within normal R820T2 tuner range — no direct sampling needed
liquid-dsp (github.com/jgaeddert/liquid-dsp):
- 2.2k stars, MIT license, actively maintained (v1.7.0, Feb 2025)
- Zero dependencies beyond standard C + math
- Provides every DSP primitive needed:
| Need | liquid-dsp Object | Function |
|---|---|---|
| Frequency translation | nco_crcf |
Mix channel to baseband |
| Low-pass filter | firfilt_crcf |
Channel isolation before decimation |
| Decimation | msresamp_crcf |
Reduce sample rate to audio |
| FM demodulation | freqdem |
Phase discriminator |
| Squelch | agc_crcf |
6-state squelch machine (RISE/SIGNALHI/FALL/SIGNALLO/TIMEOUT/ENABLED) |
Performance Estimate (Pi 400)
The Cortex-A72 @ 1.8 GHz with NEON does ~7 GFLOPS. Total DSP load for 4 channels:
| Operation | Per channel | 4 channels |
|---|---|---|
| NCO mix-down (960 kHz) | ~2 MFLOPS | ~8 MFLOPS |
| FIR filter + decimate (64 taps, 960→12.5 kHz) | ~60 MFLOPS | ~240 MFLOPS |
| FM demod (12.5 kHz) | negligible | negligible |
| Squelch + WAV I/O | negligible | negligible |
| Total | ~250 MFLOPS (~3.5% of Pi 400 capacity) |
This leaves massive headroom. Even 10 channels would be fine.
Estimated Scope
| Component | Lines of C |
|---|---|
| Main + config parsing (YAML) | ~150 |
| RTL-SDR setup + async callback + ring buffer | ~150 |
| Per-channel DSP pipeline | ~200 |
| Squelch state machine + hang time | ~100 |
| WAV file writer (headers, open/close) | ~150 |
| Threading + signal handling + cleanup | ~100 |
| Configuration structs + channel definitions | ~100 |
| Total | ~950 lines |
Dependencies: librtlsdr, liquid-dsp, pthreads, libyaml (or a minimal YAML parser). No GNU Radio, no Python, no heavyweight frameworks.
Reference Projects
- sdrx (github.com/johanhedin/sdrx) — Small C++ multichannel AM receiver, tested on Pi 4 and Pi Zero 2 W. Uses per-channel NCO+filter+decimate approach. Good architectural reference.
- rtl_fm (osmocom/rtl-sdr) — ~1500 lines of C, complete single-channel FM demod pipeline. Reference for FM discriminator implementation (all integer arithmetic).
- liquid-dsp examples —
agc_crcf_squelch_example.cdemonstrates the squelch state machine. Blog post by Andres Vahter shows complete FM demod pipeline with liquid-dsp.
Verdict
Best option if RTLSDR-Airband's limitations are blockers. Fully meets all 7 functional requirements. Lightweight, fast, no unnecessary dependencies. The ~950 lines of C is a manageable scope, and liquid-dsp does the heavy DSP lifting. The main cost is development time (2-4 days for experienced C, longer if learning liquid-dsp).
3. GNU Radio + Python — Viable but Heavy
Package: apt-get install gnuradio on Raspberry Pi OS 64-bit (v3.10.5.1 in Bookworm)
How It Would Work
A Python script builds a GNU Radio flowgraph:
- OsmoSDR Source → captures IQ at ~1 MHz centered on 27.185 MHz
- N × Frequency Xlating FIR Filter → isolates each CB channel, decimates to ~20 kHz
- N × NBFM Receive → demodulates NFM
- N × Power Squelch → detects activity, emits
squelch_sob/squelch_eobstream tags - Custom Embedded Python Block → implements hang time logic, converts to burst tags
- N × Tagged File Sink → writes WAV file per transmission
Strengths
- Well-established multi-channel NFM pattern
- Power Squelch block with stream tags
- Runs headless, no GUI needed
- Existing reference project: ham2mon (280 stars, does multi-channel NBFM scanning)
Limitations
- Heavy dependency — GNU Radio is a large framework (~500 MB installed)
- Python required for custom logic (hang time, file naming, YAML config) — user prefers Go or C/C++
- Performance on Pi 400: 3-4 channels at 1 MHz would use ~30-50% CPU (estimated). Feasible but much heavier than the custom C approach (~3.5% CPU)
- ham2mon on Pi: Users reported Pi 3B was too slow. Pi 400 should be OK for 3-4 channels but not with huge margin
- No built-in hang time — requires custom Python block
- WAV file naming — requires custom Python wrapper for
{frequency}_{timestamp}.wavformat - Squelch-to-burst conversion — needs the third-party squelch-to-burst embedded block
Verdict
Viable middle ground between using existing software and building from scratch. But it combines the downsides of both: you still need significant custom Python code, AND you take on the weight of the full GNU Radio framework. Only recommended if you're already familiar with GNU Radio.
4. SDRAngel Server — Capable but Over-engineered
Repository: github.com/f4exb/sdrangel
Key Findings
- Has a headless server binary (
sdrangelsrv) with full REST API — no GPU needed - Supports multi-channel NFM from a single RTL-SDR
- Has a Frequency Scanner plugin for detecting activity
- Has a Demod Analyzer for recording demodulated audio as WAV with silence-based start/stop
- Runs on Pi ARM64 (Docker images available)
- User's experience: GUI version was very heavy on GPU
Limitations
- Demod Analyzer connects to one demodulator per instance — simultaneous recording of multiple channels requires multiple instances, heavy on Pi
- No simple config file — all configuration via REST API or preset loading
- Complex architecture — massive C++/Qt codebase, overkill for 3-4 NFM channels
- First-run FFTW wisdom generation takes very long on Pi hardware
- Initial setup requires GUI (or manual XML editing) to create presets, then transfer to headless Pi
Verdict
Not recommended. Extremely powerful but far too complex for this use case. The REST API approach adds unnecessary friction for what should be a "configure once, run forever" system.
5. SDRTrunk — Not a Fit
Repository: github.com/DSheirer/sdrtrunk
Why Not
- Designed for trunked radio systems (P25, DMR) — conventional NFM monitoring means navigating trunked-radio concepts (aliases, talkgroups, playlists)
- Java/JavaFX — recommends 8-16 GB RAM; Pi 400 has 4 GB
- No native headless mode — workarounds exist but initial config requires GUI
- 27 MHz at edge of RTL-SDR tuner range — SDRTrunk's direct sampling support is poorly documented
- Over-engineered for simple conventional NFM monitoring
Verdict
Ruled out. Wrong tool for the job.
6. rtl_fm — Too Limited
What it is: Command-line FM demodulator included with librtlsdr.
Why Not
- Sequential scanning only — hops between frequencies one at a time, cannot demodulate multiple channels simultaneously
- With only 3-4 channels this is more tolerable (less missed activity), but still not ideal
- Could work as a quick-and-dirty solution:
rtl_fm -M fm -f freq1 -f freq2 -f freq3 -s 12k -l 50 | sox ...
Verdict
Fallback only. Useful for initial RF testing (does my antenna receive 27 MHz? what's a good squelch level?) but does not meet the simultaneous monitoring requirement.
7. Other Tools Considered
| Tool | Why Not |
|---|---|
| MultiFM (tsl-sdr) | ARM NEON optimized channelizer, but no squelch or recording — needs significant wrapper scripting. Better to use liquid-dsp directly in a custom build. |
| csdr | Pipe-based DSP toolkit. Flexible but fragile for production use. Running parallel pipelines via tee is inefficient. |
| rtl_power | Spectrum power logger only — no demodulation or recording. Would need a second dongle. |
| multimon-ng | Digital mode decoder (POCSAG, DTMF), not an analog FM demodulator. Irrelevant. |
| SDR# | Windows only. Ruled out. |
| Go-based custom build | Go RTL-SDR bindings unmaintained (last commit 2016). Go DSP libraries all WIP/abandoned. Would end up wrapping C libraries via cgo — adds complexity without benefit. |
Recommended Path
Phase 1: Validate RF with RTLSDR-Airband
- Install RTLSDR-Airband on the Pi 400
- Configure 3-4 CB channels with NFM mode
- Verify: Does 27 MHz reception work? What squelch levels are needed? What's the audio quality?
- Assess: Are MP3 output and its recording behavior acceptable?
If yes → done. RTLSDR-Airband is the solution.
Phase 2 (if needed): Custom C build with librtlsdr + liquid-dsp
Build a purpose-built ~950-line C application that meets all requirements exactly:
- WAV output with
{frequency}_{timestamp}.wavnaming - Configurable hang time with conversation grouping
- YAML configuration
- Console activity logging
- Minimal CPU footprint (~3.5% on Pi 400)
Use RTLSDR-Airband's FFT channelizer architecture and sdrx's per-channel approach as references.
Open Questions Resolved
| # | Original Question | Answer |
|---|---|---|
| 1 | Which existing software fits best? | RTLSDR-Airband is the closest fit |
| 2 | Simplest custom approach? | C with librtlsdr + liquid-dsp (~950 LOC) |
| 3 | Optimal sample rate? | 960 kHz covers the full CB band with margin |
| 4 | Audio sample rate for CB voice? | 8-12.5 kHz sufficient for CB voice (~3 kHz audio bandwidth) |
New Open Questions
| # | Question | Context |
|---|---|---|
| 1 | Does RTLSDR-Airband work at 27 MHz? | It's designed for VHF airband. 27 MHz is within RTL-SDR tuner range but outside RTLSDR-Airband's typical use case. Needs testing. |
| 2 | Is MP3 output acceptable? | RTLSDR-Airband only outputs MP3. If WAV is a hard requirement, custom build is needed. |
| 3 | What are the specific CB frequencies to monitor? | User to provide the 3-4 EU CB NFM frequencies. |