Decode Voice Audio
The Apollo Unified S-Band downlink carries a voice channel on a 1.25 MHz FM subcarrier alongside the PCM telemetry. The voice_subcarrier_demod block extracts this subcarrier and recovers 300—3000 Hz telephone-quality audio suitable for playback or recording.
Voice Channel Specifications
Section titled “Voice Channel Specifications”| Parameter | Value |
|---|---|
| Subcarrier frequency | 1.25 MHz (FM modulated) |
| FM deviation | +/-29 kHz |
| Audio bandwidth | 300—3000 Hz |
| Default output sample rate | 8000 Hz |
| Modulation mode | PM downlink only (not FM mode) |
On the spacecraft, the audio path runs through an FM VCO at 113 kHz, then a balanced mixer with the 512 kHz master clock, a bandpass filter, and a frequency doubler to produce the 1.25 MHz subcarrier. The receiver reverses this process: extract the subcarrier, apply an FM discriminator, and bandpass-filter the resulting audio.
Building a Voice Demodulation Flowgraph
Section titled “Building a Voice Demodulation Flowgraph”The simplest approach uses pm_demod to recover the composite modulating signal, then voice_subcarrier_demod to extract the audio.
Live Playback
Section titled “Live Playback”from gnuradio import audio, blocks, gr
from apollo.constants import SAMPLE_RATE_BASEBANDfrom apollo.pm_demod import pm_demodfrom apollo.voice_subcarrier_demod import voice_subcarrier_demod
tb = gr.top_block()
# Source: complex baseband from SDR or filesrc = blocks.file_source( gr.sizeof_gr_complex, "/path/to/apollo_baseband.cf32", repeat=False,)
# Stage 1: PM demodulator -- carrier PLL + phase extractionpm = pm_demod(carrier_pll_bw=0.02, sample_rate=SAMPLE_RATE_BASEBAND)
# Stage 2: Voice subcarrier demod -- extract 1.25 MHz FM, output 8 kHz audiovoice = voice_subcarrier_demod( sample_rate=SAMPLE_RATE_BASEBAND, audio_rate=8000,)
# Stage 3: Audio sink for live playbackaudio_sink = audio.sink(8000)
tb.connect(src, pm, voice, audio_sink)tb.run()Record to WAV File
Section titled “Record to WAV File”To save the decoded audio to a file instead of playing it live:
from gnuradio import blocks, gr
from apollo.constants import SAMPLE_RATE_BASEBANDfrom apollo.pm_demod import pm_demodfrom apollo.voice_subcarrier_demod import voice_subcarrier_demod
tb = gr.top_block()
src = blocks.file_source( gr.sizeof_gr_complex, "/path/to/apollo_baseband.cf32", repeat=False,)
pm = pm_demod(carrier_pll_bw=0.02, sample_rate=SAMPLE_RATE_BASEBAND)
voice = voice_subcarrier_demod( sample_rate=SAMPLE_RATE_BASEBAND, audio_rate=8000,)
# WAV file sink -- 1 channel, 8 kHz, 16-bit PCMwav_sink = blocks.wavfile_sink( "/path/to/apollo_voice.wav", 1, # channels 8000, # sample rate blocks.FORMAT_WAV, blocks.FORMAT_PCM_16,)
tb.connect(src, pm, voice, wav_sink)tb.run()Using a Synthetic Test Signal
Section titled “Using a Synthetic Test Signal”To test the voice demod without a real recording, generate a signal with the voice subcarrier enabled:
from gnuradio import blocks, gr
from apollo.constants import SAMPLE_RATE_BASEBANDfrom apollo.pm_demod import pm_demodfrom apollo.usb_signal_gen import generate_usb_basebandfrom apollo.voice_subcarrier_demod import voice_subcarrier_demod
# Generate 100 frames (~2 seconds) with a 1 kHz voice tonesignal, _ = generate_usb_baseband( frames=100, voice_enabled=True, voice_tone_hz=1000.0, snr_db=30.0,)
tb = gr.top_block()
src = blocks.vector_source_c(signal.tolist())pm = pm_demod(sample_rate=SAMPLE_RATE_BASEBAND)voice = voice_subcarrier_demod(sample_rate=SAMPLE_RATE_BASEBAND, audio_rate=8000)
wav_sink = blocks.wavfile_sink( "test_voice_output.wav", 1, 8000, blocks.FORMAT_WAV, blocks.FORMAT_PCM_16,)
tb.connect(src, pm, voice, wav_sink)tb.run()
print("Wrote test_voice_output.wav")The output WAV file should contain a clean 1 kHz tone.
Internal Processing Chain
Section titled “Internal Processing Chain”The voice_subcarrier_demod block is a hierarchical block that chains four stages internally:
flowchart LR
A["PM Demod<br/>Output (float)"] --> B["Subcarrier Extract<br/>BPF 1.25 MHz<br/>BW = 58 kHz<br/>Decimation ~40x"]
B --> C["Quadrature Demod<br/>FM discriminator"]
C --> D["Audio BPF<br/>300-3000 Hz"]
D --> E["Rational Resampler<br/>to 8000 Hz"]
E --> F["Audio<br/>Output (float)"]
The aggressive decimation in stage 1 (from 5.12 MHz down to ~128 kHz) reduces the processing load before the FM discriminator runs. The audio bandpass removes DC offset from the discriminator and any noise above 3 kHz.
Parameters
Section titled “Parameters”The voice_subcarrier_demod constructor accepts two parameters:
| Parameter | Default | Description |
|---|---|---|
sample_rate | 5,120,000 | Input sample rate from PM demodulator |
audio_rate | 8000 | Output audio sample rate in Hz |
The subcarrier frequency (1.25 MHz), FM deviation (+/-29 kHz), and audio bandwidth (300—3000 Hz) are fixed to the Apollo specification and are not configurable.
You can query the actual output rate and intermediate rate at runtime:
voice = voice_subcarrier_demod(sample_rate=5_120_000, audio_rate=8000)
print(f"Output rate: {voice.output_sample_rate} Hz") # 8000.0print(f"Extracted rate: {voice.extracted_rate} Hz") # ~128000.0Extracting Voice Alongside PCM Telemetry
Section titled “Extracting Voice Alongside PCM Telemetry”Since the voice and PCM subcarriers occupy different frequency bands, you can decode both simultaneously by splitting the PM demodulator output:
from gnuradio import blocks, gr
from apollo.constants import SAMPLE_RATE_BASEBANDfrom apollo.pm_demod import pm_demodfrom apollo.usb_downlink_receiver import usb_downlink_receiverfrom apollo.voice_subcarrier_demod import voice_subcarrier_demod
tb = gr.top_block()
src = blocks.file_source(gr.sizeof_gr_complex, "recording.cf32", repeat=False)
# Shared PM demodpm = pm_demod(sample_rate=SAMPLE_RATE_BASEBAND)
# PCM telemetry path (uses the full receiver for convenience)# Note: usb_downlink_receiver has its own PM demod internally,# so for combined use, build the chain with individual blocks.from apollo.bpsk_subcarrier_demod import bpsk_subcarrier_demodfrom apollo.pcm_demux import pcm_demuxfrom apollo.pcm_frame_sync import pcm_frame_sync
bpsk = bpsk_subcarrier_demod(sample_rate=SAMPLE_RATE_BASEBAND)fsync = pcm_frame_sync(bit_rate=51200)demux = pcm_demux(output_format="scaled")
# Voice pathvoice = voice_subcarrier_demod(sample_rate=SAMPLE_RATE_BASEBAND)wav = blocks.wavfile_sink("voice.wav", 1, 8000, blocks.FORMAT_WAV, blocks.FORMAT_PCM_16)
# Frame outputframe_sink = blocks.message_debug()
# Connect shared PM demodtb.connect(src, pm)
# Split PM output to both pathstb.connect(pm, bpsk, fsync)tb.connect(pm, voice, wav)
# Message connections for telemetrytb.msg_connect(fsync, "frames", demux, "frames")tb.msg_connect(fsync, "frames", frame_sink, "store")
tb.run()print(f"Decoded {frame_sink.num_messages()} PCM frames")print("Wrote voice.wav")Modulating Voice (TX Side)
Section titled “Modulating Voice (TX Side)”The transmit-side counterpart to voice_subcarrier_demod is fm_voice_subcarrier_mod. It FM-modulates audio onto the 1.25 MHz subcarrier — the exact inverse of the receive chain.
Internal Test Tone
Section titled “Internal Test Tone”For testing without an audio source, the modulator generates a sine tone internally:
from apollo.fm_voice_subcarrier_mod import fm_voice_subcarrier_mod
# Source block — no input neededvoice_mod = fm_voice_subcarrier_mod(tone_freq=1000.0)External Audio Input
Section titled “External Audio Input”To modulate real audio (like Apollo mission crew recordings), set audio_input=True. This changes the block from a source to a filter that accepts a float input:
from gnuradio import blocks, gr
from apollo.fm_voice_subcarrier_mod import fm_voice_subcarrier_mod
voice_mod = fm_voice_subcarrier_mod(audio_input=True)
# Audio must be upsampled to the baseband rate (5.12 MHz)# before feeding into the modulator.# See examples/voice_subcarrier_demo.py for the full pipeline.Audio Quality Notes
Section titled “Audio Quality Notes”The recovered audio has telephone-grade quality (300—3000 Hz, 8 kHz sample rate). This matches the original system design — the Apollo voice link was optimized for intelligibility, not high fidelity. Expect the following characteristics:
- No low-frequency content below 300 Hz (filtered by design)
- No high-frequency content above 3 kHz (filtered by design)
- FM noise floor depends on the input signal SNR
- The 200 Hz transition bands in the audio BPF may cause slight rolloff near the band edges
For the best audio quality from a noisy recording, use a narrower carrier PLL bandwidth (0.01) in the PM demod to minimize phase noise that propagates into the voice demodulator.