A hands-on course for conquering the RF spectrum β learn to demodulate IQ and QAM signals with GNU Radio and SDRangel, then prove it against realistic captures of LTE, digital video (DVB), and Wi-Fi. Part 2 of the RF Range series (follow-on to Coldferry).
Basaltwindow takes you from "I can see a signal on a waterfall" to "I can pull the bits out of it" β complex IQ, quadrature amplitude modulation, receiver synchronization, and real physical-layer decode (an LTE Physical Cell ID and a DVB transport-stream program map), all from raw samples.
π New to this? Start with QAM in an Afternoon
A gentle 2β4 hour, no-hardware, no-math-up-front on-ramp that turns radio noise into bits and makes QAM click. The full 62-hour Program of Instruction is here when you want the deep version β but you don't need it to get started.
- Start here: QAM in an afternoon
- Quickstart (no hardware needed)
- What you get
- The signal library
- Two real decode wins (and one easter egg)
- The QAM receiver
- Program of Instruction
- Hardware fleet
- The honest decode boundary
- Reproduce the verification
- Repository layout
- License & disclaimer
If 62 hours sounds like a lot, it should β that's the full program. Most people
should start with the QAM Fast Track (Basaltwindow_QAM_Fast_Track.docx):
a 2β4 hour, hardware-free, confidence-first course built on one idea β
QAM is just dots on a plane, and a receiver's only job is to un-blur them.
| Time | You'll be able to⦠|
|---|---|
| 2 h | See, explain, and run a QAM demodulator in simulation |
| 3 h | β¦plus demodulate a captured signal yourself and read out bits |
| 4 h | β¦plus decode something real (a DVB service, or an LTE cell ID) |
No math up front, no SDR required β it all runs in the loopback simulation.
# 1. Install GNU Radio 3.10 (Ubuntu 22.04/24.04)
sudo apt install gnuradio soapysdr-tools
# 2. See a QAM receiver work, start to finish, in pure simulation
gnuradio-companion flowgraphs/qam_loopback_sim.grc # press the βΆ button
# 3. Demodulate a "captured" signal from file
gnuradio-companion flowgraphs/qam_rx_demod.grc # point File Source at an iq_samples/*.iq
# 4. Prove it numerically (EVM, LTE cell-ID decode, DVB transport-stream parse)
python3 verify_demod.py
python3 verify_signals.pyYou will watch a noisy, rotating blur collapse into a clean QPSK constellation β the whole point of a receiver, in one picture:
| Piece | What it is |
|---|---|
Lab Guide (Basaltwindow_Lab_Guide.docx) |
13-page student manual: IQ & QAM from fundamentals, per-radio setup, four exercises (sim β bench β range β SDRangel), block reference, troubleshooting, safety. |
Program of Instruction (docs/PROGRAM_OF_INSTRUCTION.md, Basaltwindow_POI.docx) |
Full ~62-hour curriculum: objectives, 12 blocks of instruction, labs, assessments, capstone, schedule. |
GNU Radio flowgraphs (flowgraphs/) |
Loopback simulation, QAM transmitter, and QAM receiver (.grc + generated .py). |
SDRangel config (sdrangel/) |
REST-API script + guide to drive SDRangel as a demod station. |
Signal library (iq_samples/) |
8 synthetic-but-standards-faithful IQ captures + ground-truth transport streams. |
| Tools | parse_ts.py (MPEG-TS parser), verify_*.py (decode proofs), generators. |
Reference card (signal_reference_card.png) |
PSD + spectrogram of every signal β a recognition sheet. |
Every capture is interleaved complex float32 (GNU Radio's native complex type);
the sample rate is in each filename. They are synthetic but standards-faithful β
correct bandwidths, framing, cyclic prefix, real LTE sync signals, real Wi-Fi
preambles, and valid MPEG transport streams.
| Capture | Standard | Sample rate | Decodes to |
|---|---|---|---|
lte_fdd_5MHz_qpsk_7p68Msps |
LTE FDD 5 MHz DL | 7.68 Msps | Physical Cell ID 126 |
lte_fdd_10MHz_64qam_15p36Msps |
LTE FDD 10 MHz DL | 15.36 Msps | Physical Cell ID 166 |
lte_fdd_20MHz_16qam_30p72Msps |
LTE FDD 20 MHz DL | 30.72 Msps | Physical Cell ID 302 |
dvbs_video_qpsk_2Msym_4Msps |
DVB-S-style video | 4.0 Msps | MPEG-TS program map (H.264 + AAC) |
dvbs_rickroll_qpsk_2Msym_4Msps |
DVB-S-style video | 4.0 Msps | service "Never Gonna Give You Up" + text π΅ |
wifi_80211g_20MHz_20Msps |
802.11g Wi-Fi | 20.0 Msps | preamble / bursts |
qpsk_2Msps, qam16_2Msps |
Generic QPSK / 16-QAM | 2.0 Msps | training symbols |
See iq_samples/SIGNAL_MANIFEST.md for full details.
Recovering a constellation is the hard, universal part of receiving any digital signal. Basaltwindow lets you go one step past it and read something a real network actually broadcasts:
- LTE Physical Cell ID. The LTE captures carry real PSS (Zadoff-Chu) and SSS
(m-sequences per 3GPP 36.211). A cell search recovers
PCI = 3Β·N_ID_1 + N_ID_2β 126, 166, 302 β exactly what a real handset computes at power-on. - DVB transport stream. The video captures carry a valid MPEG-TS (PAT, PMT, SDT,
correct CRCs). Demodulate the QPSK, byte-sync on
0x47, andpython3 parse_ts.pyprints the program map (H.264 video, AAC audio).
Easter egg. The
dvbs_rickrollcapture's DVB service name is "Never Gonna Give You Up" and its data stream carries a message. Demodulate and parse it and your own receiver rickrolls you β while teaching DVB service information and payload extraction. (The payload is original text; no song is embedded.)
$ python3 parse_ts.py iq_samples/dvbs_rickroll_reference.ts
Service (SDT): "Never Gonna Give You Up" provider "RickTV"
elementary stream PID 0x0100 type 0x1B (H.264 video)
elementary stream PID 0x0101 type 0x0F (AAC audio)
elementary stream PID 0x00A0 type 0x06 (PES private data)
Embedded data-stream text:
=== OPERATION BASALTWINDOW / RF RANGE -- DECODE CONFIRMED === ...
The five jobs a receiver must do, in order β exactly the blocks you wire up:
SDR ββΆ AGC ββΆ FLL band-edge ββΆ polyphase clock sync ββΆ CMA equalizer ββΆ Costas loop ββΆ slicer ββΆ bits
level coarse carrier symbol timing channel fine carrier decision
verify_demod.py runs this chain on a noisy, frequency-offset QPSK signal and
measures the result: ~13% EVM, 99.9% of symbols in tight clusters β locked.
Two ways in, depending on your time:
- QAM Fast Track β the 2β4 hour on-ramp. Start here.
- Full Program of Instruction β the complete
~62-hour, 8-day curriculum (also
Basaltwindow_POI.docx): course goals, prerequisites, 12 blocks of instruction with enabling objectives, hands-on labs mapped to this repo, an assessment strategy, a capstone, an equipment list, and references.
| Radio | TX/RX | Role |
|---|---|---|
| RTL-SDR | RX only (β€ 2.4 Msps) | The cheap demod station β one per student. |
| HackRF One | TX & RX, half-duplex (1 MHzβ6 GHz) | Range transmitter / receiver. |
| AntSDR E200 | TX & RX, full-duplex (Pluto/IIO) | Bench-loopback star β TX and RX at once. |
| KrakenSDR | RX only (5 coherent) | Direction-finding extension (not core). |
Reading captures from file works on any workstation. Over-the-air replay of the wideband captures (15.36 / 30.72 / 20 Msps) needs HackRF or AntSDR.
You genuinely recover a real cell ID and a real program map from raw IQ β the same operations real receivers perform. What these captures deliberately omit is the FEC / scrambling / higher-layer stack that turns a program map into playable video or a PCI into a cell's full system information. On real signals those are extra, well-defined steps (srsRAN for LTE, a DVB decoder for video) β not a different skill. The demodulation you practice here is the physical layer all of it stands on.
python3.12 make_signals.py # regenerate the IQ captures + reference transport streams
python3.12 verify_signals.py # LTE cell-ID decode, DVB TS parse, Wi-Fi detection, reference card
python3 verify_demod.py # QAM receiver EVM (needs GNU Radio python bindings)
node build_guide.js # rebuild the lab guide docx
python3.12 make_banner.py # rebuild this bannerLatest verification output is checked in at
iq_samples/VERIFICATION_REPORT.txt.
GNU Radio's Python bindings on Ubuntu 24.04 are built for CPython 3.12 β run the signal scripts with
python3.12.
operation-basaltwindow/
βββ README.md β you are here
βββ assets/banner.png
βββ docs/PROGRAM_OF_INSTRUCTION.md
βββ Basaltwindow_Lab_Guide.docx β student lab guide
βββ Basaltwindow_POI.docx β program of instruction (Word)
βββ flowgraphs/ β GNU Radio .grc (+ generated .py)
β βββ qam_loopback_sim.grc
β βββ qam_tx_range.grc
β βββ qam_rx_demod.grc
βββ sdrangel/ β SDRangel REST config + guide
βββ iq_samples/ β the signal library + manifest + reference TS
βββ parse_ts.py β MPEG-TS / DVB SDT parser
βββ verify_demod.py β receiver EVM proof
βββ verify_signals.py β LTE/DVB/Wi-Fi decode proofs + reference card
βββ make_signals.py β signal generator
βββ gen_flowgraphs.py β flowgraph generator
βββ make_assets.py / make_banner.py / build_guide.js
βββ signal_reference_card.png
βββ constellation_before_after.png
Code and documents are released under the MIT License (see LICENSE).
Disclaimer. All IQ captures in this repository are synthetic β generated by
make_signals.py. They are standards-faithful for teaching but are not real
intercepts and carry no real user data. The "rickroll" capture contains only
original text and a DVB service name; no song or copyrighted media is embedded.
Transmitting any signal over the air is subject to authorization β see the Lab Guide,
Appendix B.


