Files
sdr-recorder/RESEARCH.md
Michael Smith 19fcae6e88 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>
2026-03-17 18:20:03 +00:00

17 KiB
Raw Permalink Blame History

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 — 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 controlsplit_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):

  • 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 examplesagc_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 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.

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.