Skip to content

Run the Demos

gr-apollo ships with eight demo scripts that exercise different parts of the TX/RX chain. They progress from a self-contained streaming loopback to a full mission simulation with crew voice, live integration with the Virtual AGC emulator, uplink command encoding, and PRN ranging.

DemoRequiresWhat It Does
loopback_demo.pyGNU RadioStreaming TX to RX round-trip
voice_subcarrier_demo.pyGNU Radio, scipyReal audio through 1.25 MHz FM
full_downlink_demo.pyGNU Radio, scipyPCM telemetry + crew voice on one carrier
agc_loopback_demo.pyyaAGC (no GR)Live AGC telemetry over TCP
fetch_apollo_audio.pyffmpegDownload real Apollo recordings from Archive.org
real_signal_demo.pyGNU Radio, scipyProcess real Apollo audio through full USB chain
uplink_loopback_demo.pyGNU RadioEncode DSKY commands, modulate, demodulate, verify
ranging_demo.pyNone (pure Python)PRN code generation, delay simulation, correlation

Script: examples/loopback_demo.py Requires: GNU Radio

The loopback demo connects usb_signal_source directly to usb_downlink_receiver through the GNU Radio scheduler. It transmits PCM frames, receives them back, and displays sync word analysis for each recovered frame.

graph LR
    A["usb_signal_source\n(TX chain)"]:::rf --> B["head\n(sample limiter)"]:::timing --> C["usb_downlink_receiver\n(RX chain)"]:::rf
    C -->|"frames (PDU)"| D["message_debug\n(store)"]:::data

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
Terminal window
uv run python examples/loopback_demo.py
uv run python examples/loopback_demo.py --voice # include voice subcarrier
uv run python examples/loopback_demo.py --snr 20 # add noise at 20 dB SNR
uv run python examples/loopback_demo.py --frames 20 # generate 20 frames
ArgumentDefaultDescription
--frames10Number of PCM frames to transmit
--snrNoneSNR in dB (None = clean, no noise)
--voiceoffEnable the 1.25 MHz FM voice subcarrier with 1 kHz test tone
============================================================
Apollo USB Loopback Demo
============================================================
Frames to transmit: 10
Samples per frame: 102,400
Total samples: 1,024,000
Duration: 0.200 s
SNR: clean (no noise)
Voice subcarrier: disabled
Building flowgraph...
Running flowgraph (TX -> RX)...
Recovered 8 frames from 10 transmitted
------------------------------------------------------------
Frame 1: ID= 3 (odd ), sync=0xAB31D403, 124 words [00 00 00 00 00 00 00 00 ...]
Frame 2: ID= 4 (even), sync=0xABCED404, 124 words [00 00 00 00 00 00 00 00 ...]
...
------------------------------------------------------------
Recovery rate: 8/10 (80%)

Script: examples/voice_subcarrier_demo.py Requires: GNU Radio, scipy

This demo takes a real audio file (such as actual Apollo 11 crew recordings), modulates it onto the 1.25 MHz FM voice subcarrier with +/-29 kHz deviation, then demodulates it back to audio. The round-trip exercises the same signal path the spacecraft and ground station used.

graph LR
    A["WAV file\n(any rate)"]:::data --> B["resample\nto 8 kHz"]:::timing --> C["upsample\nto 5.12 MHz"]:::timing
    C --> D["fm_voice_subcarrier_mod\n(audio_input=True)"]:::rf
    D --> E["voice_subcarrier_demod\n(8 kHz output)"]:::rf
    E --> F["recovered\nWAV file"]:::data

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
Terminal window
uv run python examples/voice_subcarrier_demo.py examples/audio/apollo11_crew.wav
uv run python examples/voice_subcarrier_demo.py input.wav --output recovered.wav
uv run python examples/voice_subcarrier_demo.py input.wav --play
ArgumentDefaultDescription
input (positional)Input WAV file (any sample rate)
--output, -o<input>_recovered.wavOutput WAV file path
--playoffPlay recovered audio with aplay after processing
--sample-rate5,120,000Baseband sample rate in Hz
============================================================
Apollo Voice Subcarrier Demo
============================================================
Input: examples/audio/apollo11_crew.wav
Sample rate: 48000 Hz
Duration: 5.23 s
Samples: 251,136
Resampled to 8000 Hz: 41,856 samples
Upsampling 8000 Hz -> 5.12 MHz (ratio 640:1)...
Upsampled: 26,787,840 samples (2.1s)
Building flowgraph: FM mod (1.25 MHz) -> FM demod...
Running flowgraph...
Processed in 3.4s
Recovered: 41,790 samples at 8000 Hz
Duration: 5.22 s
Saved: examples/audio/apollo11_crew_recovered.wav
Peak amplitude: 0.8234
Play with: aplay examples/audio/apollo11_crew_recovered.wav
Done.

Script: examples/full_downlink_demo.py Requires: GNU Radio, scipy

The full downlink demo reconstructs the complete Apollo USB downlink: PCM telemetry frames on the 1.024 MHz BPSK subcarrier AND crew voice on the 1.25 MHz FM subcarrier, both phase-modulated onto a single complex carrier. The receiver splits the signal back into decoded frames and audio.

graph TB
    subgraph TX ["TX (spacecraft)"]
        direction LR
        A["pcm_frame_source\n→ nrz → bpsk_mod"]:::data --> D["add_ff"]:::rf
        B["crew audio\n→ fm_voice_mod"]:::data --> C["× 0.764"]:::rf --> D
        D --> E["pm_mod"]:::rf
    end

    subgraph RX ["RX (ground station)"]
        direction LR
        F["pm_demod"]:::rf --> G["bpsk_demod\n→ frame_sync"]:::rf
        F --> H["voice_demod"]:::rf
        G --> I["PCM frames"]:::data
        H --> J["crew audio"]:::data
    end

    E --> F

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff

This demo builds the TX chain manually (not using usb_signal_source) so it can inject external audio into the voice channel. It then runs the RX chain twice: once for PCM frame recovery, once for voice demodulation.

Terminal window
uv run python examples/full_downlink_demo.py examples/audio/apollo11_crew.wav
uv run python examples/full_downlink_demo.py input.wav --snr 25
uv run python examples/full_downlink_demo.py input.wav --play
ArgumentDefaultDescription
audio (positional)Input crew voice WAV file
--output, -o<input>_fullchain.wavOutput WAV path for recovered voice
--snrNoneAdd AWGN noise at this SNR in dB
--playoffPlay recovered voice with aplay
============================================================
Apollo Full Downlink Demo
PCM telemetry (1.024 MHz BPSK) + crew voice (1.25 MHz FM)
============================================================
Loading crew voice audio...
Source: examples/audio/apollo11_crew.wav (5.23s)
Upsampled: 26,787,840 samples at 5.12 MHz
PCM frames: ~263 at 50 fps
Signal: 26,787,840 samples (5.23s)
SNR: clean
TX: Building combined PCM + voice signal...
Generated 26,787,840 complex samples (4.2s)
PM envelope std: 0.000001 (should be ~0 for clean)
RX: Decoding PCM telemetry frames...
Recovered 260 PCM frames (6.1s)
Frame 1: ID= 3 (odd), 124 data words
Frame 2: ID= 4 (even), 124 data words
Frame 3: ID= 5 (odd), 124 data words
Frame 4: ID= 6 (even), 124 data words
Frame 5: ID= 7 (odd), 124 data words
... (255 more frames)
RX: Demodulating crew voice (1.25 MHz FM)...
Recovered 41,790 audio samples (3.8s)
Duration: 5.22s at 8000 Hz
Saved: examples/audio/apollo11_crew_fullchain.wav
============================================================
TX: 5.23s of combined PCM + voice
RX: 260 PCM frames + 5.22s crew voice
SNR: clean
============================================================
Play voice: aplay examples/audio/apollo11_crew_fullchain.wav

Script: examples/agc_loopback_demo.py Requires: yaAGC emulator (no GNU Radio needed)

This demo connects directly to a running Virtual AGC emulator over TCP, receives DNTM1/DNTM2 telemetry packets, decodes them into downlink list snapshots, and optionally sends DSKY commands.

graph LR
    A["yaAGC\n(Luminary099)"]:::timing -->|"TCP :19697"| B["AGCBridgeClient"]:::rf
    B --> C["DownlinkEngine\n(reassemble words)"]:::data
    C --> D["telemetry\nsnapshots"]:::data
    E["UplinkEncoder\n(V16N36E)"]:::data -->|"INLINK ch 045"| B

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
  1. Install Virtual AGC from the project website. The key binary is yaAGC.

  2. Start the AGC emulator with a mission flight software image:

    Terminal window
    yaAGC --core=Luminary099.bin --port=19697
  3. Optionally start yaDSKY2 for a visual DSKY display:

    Terminal window
    yaDSKY2 --port=19698
Terminal window
uv run python examples/agc_loopback_demo.py
uv run python examples/agc_loopback_demo.py --host 192.168.1.100
uv run python examples/agc_loopback_demo.py --send-v16n36
uv run python examples/agc_loopback_demo.py --duration 30
ArgumentDefaultDescription
--hostlocalhostyaAGC hostname or IP
--port19697yaAGC TCP port
--duration10.0Collection duration in seconds
--send-v16n36offSend V16N36E (display mission elapsed time) to the AGC
============================================================
Apollo AGC Integration Demo
============================================================
Target: localhost:19697
Duration: 10.0 seconds
Connecting to yaAGC at localhost:19697...
Connection: connecting
Connection: connected
Sending V16N36E (display time)...
Sent 7 uplink words
Collecting telemetry for 10.0 seconds...
------------------------------------------------------------
Telemetry snapshot: CM Coast/Alignment (type 2), 400 words
[000] = 00002 (2)
[001] = 00000 (0)
[002] = 77777 (32767)
[003] = 00000 (0)
[004] = 00000 (0)
... (395 more words)
------------------------------------------------------------
Summary:
Total packets received: 1247
Telemetry words: 834
Telemetry snapshots: 2
Duration: 10.1 seconds
Done.

Script: examples/fetch_apollo_audio.py Requires: ffmpeg

This utility downloads Apollo 11 audio highlights from the Internet Archive as a FLAC file, then extracts individual clips using ffmpeg. The clips are saved as 48 kHz mono WAV files in examples/audio/ for use with the other signal processing demos.

graph LR
    A["Archive.org\nApollo11Highlights.flac"]:::data --> B["urllib\n(download)"]:::timing --> C["FLAC file\n(local)"]:::data
    C --> D["ffmpeg\n(seek + extract)"]:::rf --> E["apollo11_liftoff.wav"]:::data
    C --> F["ffmpeg\n(seek + extract)"]:::rf --> G["apollo11_eagle_has_landed.wav"]:::data
    C --> H["ffmpeg\n(seek + extract)"]:::rf --> I["apollo11_one_small_step.wav"]:::data

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff

Five clips are defined in the script, covering key mission moments from liftoff through splashdown. The FLAC source is removed after extraction by default to save disk space.

Terminal window
uv run python examples/fetch_apollo_audio.py --list
uv run python examples/fetch_apollo_audio.py --all
uv run python examples/fetch_apollo_audio.py --clip eagle_has_landed
uv run python examples/fetch_apollo_audio.py --clip liftoff --force
uv run python examples/fetch_apollo_audio.py --all --keep-flac
ArgumentDefaultDescription
--listoffList available clip names and timestamps
--clipExtract a specific clip by name
--alloffExtract all five defined clips
--keep-flacoffKeep the downloaded FLAC file after extraction
--forceoffRe-download and re-extract even if files already exist
--output-direxamples/audio/Output directory for WAV files
Available clips:
liftoff 00:00:05 (00:00:30) Apollo 11 liftoff
eagle_has_landed 00:06:45 (00:00:30) The Eagle has landed
one_small_step 00:15:30 (00:00:25) One small step for man
houston_problem 00:20:00 (00:00:15) Houston, we've had a problem
splashdown 00:42:00 (00:00:20) Splashdown
5 clips defined.
Extract with: --clip NAME or --all
============================================================
Apollo 11 Audio Fetch
============================================================
Step 1: Download source FLAC
Downloading: https://archive.org/download/Apollo11AudioHighlights/Apollo11Highlights.flac
Saving to: examples/audio/Apollo11Highlights.flac
[########################################] 100.0% 45.2/45.2 MB
Downloaded 45.2 MB
Step 2: Extract 5 clip(s)
[liftoff] Extracting: Apollo 11 liftoff
start=00:00:05 duration=00:00:30
-> examples/audio/apollo11_liftoff.wav (2880 KB)
[eagle_has_landed] Extracting: The Eagle has landed
start=00:06:45 duration=00:00:30
-> examples/audio/apollo11_eagle_has_landed.wav (2880 KB)
[one_small_step] Extracting: One small step for man
start=00:15:30 duration=00:00:25
-> examples/audio/apollo11_one_small_step.wav (2400 KB)
[houston_problem] Extracting: Houston, we've had a problem
start=00:20:00 duration=00:00:15
-> examples/audio/apollo11_houston_problem.wav (1440 KB)
[splashdown] Extracting: Splashdown
start=00:42:00 duration=00:00:20
-> examples/audio/apollo11_splashdown.wav (1920 KB)
Removed source FLAC (45.2 MB). Use --keep-flac to retain.
============================================================
Extracted: 5 Failed: 0
Output: examples/audio/apollo11_*.wav
============================================================

Script: examples/real_signal_demo.py Requires: GNU Radio, scipy

This demo auto-discovers WAV files in examples/audio/ (downloaded by fetch_apollo_audio.py) and runs them through the full USB downlink chain: transmit (NRZ + BPSK + voice FM onto PM carrier) then receive (PCM frame recovery + voice demodulation). It proves the gr-apollo signal chain works on real-world audio, not just synthetic test tones.

If no downloaded clips are found, the demo falls back to the bundled examples/audio/apollo11_crew.wav.

graph TB
    subgraph discover ["Audio Discovery"]
        direction LR
        A["examples/audio/\napollo11_*.wav"]:::data --> B["auto-discover\n(skip output files)"]:::timing
    end

    subgraph TX ["TX (spacecraft)"]
        direction LR
        C["pcm_frame_source\n→ nrz → bpsk_mod"]:::data --> F["add_ff"]:::rf
        D["real audio clip\n→ resample → upsample"]:::data --> E["fm_voice_mod\n× 0.764"]:::rf --> F
        F --> G["pm_mod"]:::rf
    end

    subgraph RX ["RX (ground station)"]
        direction LR
        H["pm_demod"]:::rf --> I["bpsk_demod\n→ frame_sync"]:::rf
        H --> J["voice_demod"]:::rf
        I --> K["PCM frames"]:::data
        J --> L["recovered WAV"]:::data
    end

    B --> D
    G --> H

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff
Terminal window
uv run python examples/real_signal_demo.py
uv run python examples/real_signal_demo.py --clip eagle_has_landed
uv run python examples/real_signal_demo.py --snr 25
uv run python examples/real_signal_demo.py --clip liftoff --play
ArgumentDefaultDescription
--clipfirst discoveredProcess a specific clip by name
--snrNoneAdd AWGN noise at this SNR in dB
--playoffPlay recovered audio with aplay after processing
============================================================
Apollo Real Signal Demo
Full USB downlink: PCM telemetry + crew voice
============================================================
Found 5 clip(s): liftoff, eagle_has_landed, one_small_step, houston_problem, splashdown
Processing: eagle_has_landed
------------------------------------------------------------
Loading: examples/audio/apollo11_eagle_has_landed.wav
Duration: 30.00s, 153,600,000 baseband samples
TX: 153,600,000 samples, ~1502 PCM frames, SNR=clean
TX complete: 153,600,000 complex samples (28.5s)
RX PCM: 1498 frames recovered (41.2s)
RX voice: 240,000 samples, 30.00s (25.1s)
Saved: examples/audio/apollo11_eagle_has_landed_recovered.wav
============================================================
Summary
============================================================
Clip: eagle_has_landed
Input duration: 30.00s
Recovered audio: 30.00s
PCM frames: 1498 recovered (expected ~1502)
SNR: clean
Processing time: TX=28.5s PCM-RX=41.2s Voice-RX=25.1s
Output: examples/audio/apollo11_eagle_has_landed_recovered.wav
============================================================
Play recovered: aplay examples/audio/apollo11_eagle_has_landed_recovered.wav

Script: examples/uplink_loopback_demo.py Requires: GNU Radio

This demo exercises the full uplink signal chain. It encodes a DSKY command (V16N36E by default), serializes the words to a bit stream, modulates through the RF path (NRZ, FM onto a 70 kHz data subcarrier, then PM), demodulates on the other end, and deserializes the recovered bits back to uplink words. The TX and RX word sequences are compared for a word-for-word match.

graph LR
    subgraph TX ["TX (ground station)"]
        direction LR
        A["UplinkEncoder\n(V16N36E)"]:::data --> B["UplinkSerializer\n(word→bits)"]:::data --> C["nrz_encoder"]:::timing
        C --> D["FM mod\n(±4 kHz)"]:::rf --> E["upconvert\n70 kHz"]:::rf --> F["pm_mod\n(1.0 rad)"]:::rf
    end

    subgraph RX ["RX (spacecraft)"]
        direction LR
        G["pm_demod"]:::rf --> H["subcarrier_extract\n70 kHz"]:::rf --> I["FM demod"]:::rf
        I --> J["matched filter\n+ slicer"]:::timing --> K["UplinkDeserializer\n(bits→words)"]:::data
    end

    F --> G

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff

The pure-Python engines handle word-to-bit conversion at the endpoints, while the GNU Radio streaming chain proves the RF modulation path works end-to-end.

Terminal window
uv run python examples/uplink_loopback_demo.py
uv run python examples/uplink_loopback_demo.py --snr 20
uv run python examples/uplink_loopback_demo.py --snr 10 --verb 37 --noun 0
ArgumentDefaultDescription
--verb16Verb number for the DSKY command
--noun36Noun number for the DSKY command
--snrNoneSNR in dB (None = clean, no noise)
============================================================
Apollo Uplink Loopback Demo
============================================================
Command: V16N36E
Uplink words: 7
SNR: clean (no noise)
TX word sequence:
[0] ch=032 val=10000 (4096) bits=001000000000000
[1] ch=032 val=10001 (4097) bits=001000000000001
[2] ch=032 val=10110 (4168) bits=001000001110000
[3] ch=032 val=00011 ( 3) bits=000000000000011
[4] ch=032 val=00110 ( 6) bits=000000000000110
[5] ch=032 val=11000 (6144) bits=001100000000000
[6] ch=032 val=10010 (4106) bits=001000000001010
Total bits: 8036 (504 data + 7532 idle)
Samples per bit: 2560
Total samples: 20,572,160
Duration: 4.018 s
Building flowgraph...
Running flowgraph (TX -> RX)...
Recovered 8036 bits from slicer
Recovered 7 words (expected 7)
RX word sequence:
[0] ch=032 val=10000 (4096) bits=001000000000000
[1] ch=032 val=10001 (4097) bits=001000000000001
[2] ch=032 val=10110 (4168) bits=001000001110000
[3] ch=032 val=00011 ( 3) bits=000000000000011
[4] ch=032 val=00110 ( 6) bits=000000000000110
[5] ch=032 val=11000 (6144) bits=001100000000000
[6] ch=032 val=10010 (4106) bits=001000000001010
------------------------------------------------------------
Words transmitted: 7
Words recovered: 7
Matches: 7/7
Word error rate: 0.0%
------------------------------------------------------------
V16N36E round-trip: all 7 words match.

Script: examples/ranging_demo.py Requires: None (pure Python, no GNU Radio needed)

This demo exercises the Apollo ranging subsystem. It generates the composite PRN ranging code from its five component codes (CL, X, A, B, C), verifies their algebraic properties, NRZ-encodes the chip stream, applies a known propagation delay to simulate spacecraft distance, optionally adds noise, and cross-correlates to recover the delay. The measured range is compared against the true range.

graph LR
    A["RangingCodeGenerator\n(CL×X×A×B×C)"]:::data --> B["NRZ encode\n(bipolar ±1)"]:::timing --> C["np.roll\n(apply delay)"]:::rf
    C --> D["+ AWGN\n(optional)"]:::rf --> E["RangingCorrelator\n(cross-correlate)"]:::data
    E --> F["range estimate\n(km)"]:::data

    classDef data fill:#2d5016,stroke:#4a8c2a,color:#fff
    classDef rf fill:#1a3a5c,stroke:#3a7abd,color:#fff
    classDef timing fill:#5c3a1a,stroke:#bd7a3a,color:#fff

The demo works at chip rate (1 sample per chip) for simplicity and speed. The composite PRN code length is 2,037,675 chips, giving an unambiguous range that covers Earth-to-Moon distances.

Terminal window
uv run python examples/ranging_demo.py
uv run python examples/ranging_demo.py --range-km 100
uv run python examples/ranging_demo.py --range-km 384400 # Moon distance
uv run python examples/ranging_demo.py --snr 20
uv run python examples/ranging_demo.py --chips 200000
ArgumentDefaultDescription
--range-km100.0Target range in km
--snrNoneSNR in dB (None = clean, no noise)
--chips50,000Number of chips for correlation
============================================================
Apollo PRN Ranging Demo
============================================================
1. Component code verification
----------------------------------------
CL: length= 2 (1s=1, 0s=1), periodic=True, balance=OK [OK]
X: length= 11 (1s=6, 0s=5), periodic=True, balance=OK [OK]
A: length= 5 (1s=3, 0s=2), periodic=True, balance=OK [OK]
B: length= 7 (1s=4, 0s=3), periodic=True, balance=OK [OK]
C: length= 5 (1s=3, 0s=2), periodic=True, balance=OK [OK]
Code length: 2,037,675 = 2*11*5*7*5*... [OK]
Composite sample (50,000 chips): balance=0.500 (ideal ~0.5)
2. Generating 50,000 PRN chips...
Generated in 2.3 ms
Ones: 25,012 / 50,000 (50.0%)
3. Simulating range: 100.0 km
Round-trip distance: 200.0 km
Round-trip time: 0.6671 ms
Delay: 6.67 chips (7 samples)
Added AWGN noise at 20 dB SNR (noise power = 0.0100)
4. Cross-correlating...
Correlation time: 4.1 ms
Peak-to-average ratio: 224.3
5. Range measurement results
----------------------------------------
True range: 100.0 km
Measured range: 104.9 km
Error: 4950.0 m
Quantization step: 14984.4 m (1 chip, two-way)
Delay (chips): 7.00
Delay (samples): 7
Correlation peak: 50000
Error is within one quantization step -- measurement is correct.
============================================================

If you are new to gr-apollo:

  1. Start with the loopback demo. It has no external dependencies beyond GNU Radio and exercises the complete TX/RX round-trip in a self-contained flowgraph.

  2. Try the voice demo with an Apollo crew recording to hear the signal processing in action. Audio files in examples/audio/ are ready to use.

  3. Run the full downlink to see both PCM and voice working together on one carrier — the way it worked on the actual spacecraft.

  4. Connect to yaAGC when you are ready to interact with a running Apollo Guidance Computer.

  5. Run the ranging demo to see PRN code generation and correlation at work. It is pure Python with no GNU Radio dependency, so it runs anywhere.

  6. Try the uplink loopback to see DSKY commands travel through the RF chain and come back intact on the other side.