Tune a HackRF to an FM broadcast station and dump every data channel it carries — not just the audio. Built for RF CTFs (DEF CON Wireless Village, 5n4ck3y, ShmooCon RF CTF) where the flag hides in a subcarrier, in the RDS RadioText, in an HD Radio data service, or in the audio itself.
An FM broadcast signal is a stack of subcarriers multiplexed into one baseband called the composite (a.k.a. MPX). stationdump FM-demodulates the channel and records the whole composite at a high sample rate, so every data channel is preserved in one file:
| In the MPX composite | Frequency | What it is |
|---|---|---|
| Mono audio (L+R) | 0–15 kHz | the program |
| Stereo pilot | 19 kHz | phase reference for stereo |
| Stereo diff (L−R) | 23–53 kHz | DSB around 38 kHz |
| RDS / RBDS | 57 kHz | station name, RadioText, PI, clock, TMC, ODA, RT+ |
| SCA / SCMO | 67 & 92 kHz | leased subcarriers (reading services, muzak, data) |
| DARC | 76 kHz | Data Radio Channel |
Alongside the analog composite, a station may also broadcast HD Radio
(IBOC) — digital OFDM sidebands carrying extra audio programs (HD1–HD4),
Program Service Data, station logos, album art, and traffic. That is a separate
data plane, decoded with nrsc5 (--hd).
A capture is broken into tracks. For a music station the boundaries come
from RDS "now playing" changes (RadioText / RT+ title+artist); a
max-duration cap (--segment, default 300 s) bounds every segment so a long
talk block can't explode the disk. Each track is timestamped and, if a database
is configured, its metadata is pushed there. This is what keeps disk bounded on
a long capture: each 3–10 min chunk is processed, its blobs shipped to object
storage, and its metadata written — then the local copy is pruned.
Boundaries are measured in signal time (samples ÷ rate), so an offline IQ
replay segments exactly like a live capture. Without redsea (or with
--track-by time) segmentation falls back to fixed --segment intervals.
Blob upload + DB writes happen on a background worker, so a slow upload never stalls the SDR read (which would drop samples).
Per track (NNNN = 1-based sequence), on local disk and/or in object storage:
trackNNNN_mpx.wav— the full composite at 240 kHz. The money file: RDS, SCA, DARC and the stereo diff are all still in here, decodable offline.trackNNNN_audio.wav— the demodulated, de-emphasized mono audio (48 kHz).trackNNNN.iq— lossless raw IQ (--raw-iq).
Capture-wide, on local disk:
rds.jsonl— every decoded RDS group (one JSON object per line) ifredseais installed.hd_aas/,nrsc5.log— HD Radio data services / logos / art (--hd).meta.json— capture parameters + the full track manifest (paths, S3 keys, timestamps, now-playing,capture_id).
Structured metadata → PostgreSQL (captures, tracks, rds_groups,
artifacts); the big blobs → S3-compatible object storage (SeaweedFS behind
https://files.weslack.team, bucket weslack-rf). The DB rows hold the S3
object keys, so you can SELECT a track and fetch its audio. Configure with
--db-url/STATIONDUMP_DB_URL and the --s3-*/STATIONDUMP_S3_* settings;
with neither, capture is fully local. See the team's Outline runbook for
credentials and connection instructions.
--s3-what(defaultauto=audio,mpx, plusiqif--raw-iq) — which blob kinds to upload; each is deleted locally after a successful upload unless--keep-local.--keep(defaultall) — local pruning for blobs not sent to S3:allkeeps them,audiodrops mpx/iq,mindeletes all local blobs.
Yes — record the audio, and stationdump does by default. In a radio CTF the
"data channels" (RDS et al.) are low-bandwidth text, but the payload very often
lives in the audio program: SSTV, DTMF, Morse, a modem/RTTY burst, content
visible only in the spectrogram, or a tone hidden at 17–22 kHz. You also cannot
re-capture a live broadcast, so capture everything the first time. The defaults
therefore record audio + full MPX + RDS; add --raw-iq when disk allows and
you want a bit-exact fallback, and --hd if the station is HD.
Use --no-audio only when you deliberately want the data channels alone.
Uses uv for everything.
uv sync # create the venv + install deps
uv run stationdump --help- hackrf tools (
hackrf_transfer) — required for live capture. - redsea — live RDS/RBDS decode.
- nrsc5 — HD Radio decode (
--hd).
If redsea/nrsc5 are absent, capture still succeeds; the MPX/raw files let you
decode later.
# 60 s dump of 96.5 MHz: MPX + audio + live RDS, local files
uv run stationdump 96.5M
# Long capture, per-track (per song), push metadata + blobs to the team backend.
# Credentials come from the environment (see the Outline runbook / your env file):
# STATIONDUMP_DB_URL, STATIONDUMP_S3_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET
uv run stationdump 96.5M --duration 3600 --segment 300
# Force fixed 5-minute chunks instead of per-song boundaries
uv run stationdump 88.5M --track-by time --segment 300
# Europe (50 us de-emphasis), data channels only, no audio
uv run stationdump 100.3M --deemph 50 --no-audio
# Also pull HD Radio HD1 data services (implies --raw-iq)
uv run stationdump 98.7M --hd --hd-program 0
# Gain / front-end tuning
uv run stationdump 91.1M --lna 40 --vga 48 --ampsox trackNNNN_mpx.wav -t raw -e signed -b 16 -c 1 - | redsea -r 240000sox trackNNNN_mpx.wav -n spectrogram -o mpx.png # pilot@19k, RDS@57k, SCA@67/92kHackRF ─cs8 IQ─▶ FIR decimate (keep ±100 kHz) ─▶ FM discriminator ─▶ MPX
│
┌─────────────────────────────────────────────────┼───────────────┐
▼ ▼ ▼ ▼
trackNNNN_mpx.wav redsea → rds.jsonl audio: LPF 15k now-playing
(RDS/SCA/DARC/L-R) + now-playing → de-emph drives track
→ trackNNNN_audio boundaries
│
per-track (background): ──────┴─▶ S3 upload + DB insert + prune
The DSP is block-streaming and stateful (see stationdump/dsp.py), so a live
stream flows through without boundary clicks. Track finalization (S3 + DB +
prune) runs on a background worker so uploads never stall the capture.
--iq-file replays a saved IQ capture through the exact same path for offline
work and testing.
uv run pytest # synthesizes a known FM signal, asserts demod recovers itReceiving and recording broadcast FM is fine in most places. Retransmitting, jamming, or decoding services you aren't authorized to is not. Know your local rules. For authorized CTF/lab use.
The Unlicense — public domain. Do whatever you want.