Add research document evaluating SDR software options

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>
This commit is contained in:
Michael Smith
2026-03-17 18:20:03 +00:00
parent 45546eef46
commit 19fcae6e88

363
RESEARCH.md Normal file
View File

@@ -0,0 +1,363 @@
# 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:
1. **RTLSDR-Airband** — existing production software that does almost exactly what we need, with minimal setup. Best for validating the RF setup quickly.
2. **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](https://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_transmission` mode 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 kHz3.2 MHz
- 27 MHz is within normal R820T2 tuner range — no direct sampling needed
**liquid-dsp** ([github.com/jgaeddert/liquid-dsp](https://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](https://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](https://github.com/osmocom/rtl-sdr/blob/master/src/rtl_fm.c)) — ~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.c` demonstrates 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:
1. **OsmoSDR Source** → captures IQ at ~1 MHz centered on 27.185 MHz
2. **N × Frequency Xlating FIR Filter** → isolates each CB channel, decimates to ~20 kHz
3. **N × NBFM Receive** → demodulates NFM
4. **N × Power Squelch** → detects activity, emits `squelch_sob`/`squelch_eob` stream tags
5. **Custom Embedded Python Block** → implements hang time logic, converts to burst tags
6. **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}.wav` format
- **Squelch-to-burst conversion** — needs the third-party [squelch-to-burst](https://github.com/SDRWaveRunner/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](https://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](https://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
1. Install RTLSDR-Airband on the Pi 400
2. Configure 3-4 CB channels with NFM mode
3. Verify: Does 27 MHz reception work? What squelch levels are needed? What's the audio quality?
4. 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}.wav` naming
- 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. |