- sco_mod: 9-channel FM subcarrier oscillator modulator (inverse of sco_demod), with round-trip tests proving voltage recovery across all channels - fm_voice_subcarrier_mod: add audio_input parameter to accept external float streams (e.g., Apollo mission voice recordings) instead of internal test tone - loopback_demo.py: streaming TX->RX round-trip with CLI for noise/voice/frames - agc_loopback_demo.py: full Virtual AGC integration via TCP bridge
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""
|
|
Apollo FM Voice Subcarrier Modulator -- 1.25 MHz FM with internal tone or external audio.
|
|
|
|
Transmit-side counterpart to voice_subcarrier_demod. FM-modulates audio onto a
|
|
1.25 MHz subcarrier and outputs the real-valued subcarrier signal.
|
|
|
|
Two modes of operation:
|
|
- Internal test tone (default): generates a sine wave for testing.
|
|
- External audio input: accepts a float stream (e.g., Apollo mission voice
|
|
recordings) and modulates it onto the subcarrier.
|
|
|
|
On the real spacecraft, voice audio (300-3000 Hz) drives an FM VCO at 113 kHz,
|
|
which is mixed with the 512 kHz master clock and doubled to produce the
|
|
1.25 MHz FM subcarrier with +/-29 kHz deviation.
|
|
|
|
Reference: IMPLEMENTATION_SPEC.md section 4.2
|
|
"""
|
|
|
|
import math
|
|
|
|
from gnuradio import analog, blocks, gr
|
|
|
|
from apollo.constants import (
|
|
SAMPLE_RATE_BASEBAND,
|
|
VOICE_FM_DEVIATION_HZ,
|
|
VOICE_SUBCARRIER_HZ,
|
|
)
|
|
|
|
|
|
class fm_voice_subcarrier_mod(gr.hier_block2):
|
|
"""FM-modulated voice subcarrier (1.25 MHz) with internal test tone or external audio.
|
|
|
|
Outputs:
|
|
float -- real-valued FM subcarrier at subcarrier_freq
|
|
|
|
Inputs (when audio_input=True):
|
|
float -- external audio signal (e.g., mission voice recordings)
|
|
|
|
When audio_input=False (default), an internal sine test tone is used.
|
|
When audio_input=True, the block accepts an external float stream -- for
|
|
example, actual Apollo mission crew voice recordings.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
sample_rate: float = SAMPLE_RATE_BASEBAND,
|
|
subcarrier_freq: float = VOICE_SUBCARRIER_HZ,
|
|
fm_deviation: float = VOICE_FM_DEVIATION_HZ,
|
|
tone_freq: float = 1000.0,
|
|
audio_input: bool = False,
|
|
):
|
|
# Choose input signature based on mode
|
|
in_sig = gr.io_signature(1, 1, gr.sizeof_float) if audio_input else gr.io_signature(0, 0, 0)
|
|
|
|
gr.hier_block2.__init__(
|
|
self,
|
|
"apollo_fm_voice_subcarrier_mod",
|
|
in_sig,
|
|
gr.io_signature(1, 1, gr.sizeof_float),
|
|
)
|
|
|
|
self._sample_rate = sample_rate
|
|
self._tone_freq = tone_freq
|
|
self._subcarrier_freq = subcarrier_freq
|
|
self._fm_deviation = fm_deviation
|
|
self._audio_input = audio_input
|
|
|
|
# FM modulator: sensitivity = 2*pi*deviation/sample_rate rad/sample
|
|
# With unit-amplitude sine input this gives +/-fm_deviation Hz.
|
|
fm_sensitivity = 2.0 * math.pi * fm_deviation / sample_rate
|
|
self.fm_mod = analog.frequency_modulator_fc(fm_sensitivity)
|
|
|
|
# LO at subcarrier frequency for upconversion
|
|
self.lo = analog.sig_source_c(
|
|
sample_rate, analog.GR_COS_WAVE, subcarrier_freq, 1.0, 0,
|
|
)
|
|
|
|
# Mixer: FM baseband x LO -> subcarrier
|
|
self.mixer = blocks.multiply_cc(1)
|
|
|
|
# Extract real part for float output
|
|
self.to_real = blocks.complex_to_real(1)
|
|
|
|
if audio_input:
|
|
# External audio input -> FM mod
|
|
self.connect(self, self.fm_mod, (self.mixer, 0))
|
|
else:
|
|
# Internal test tone -> FM mod
|
|
self.tone = analog.sig_source_f(
|
|
sample_rate, analog.GR_SIN_WAVE, tone_freq, 1.0, 0,
|
|
)
|
|
self.connect(self.tone, self.fm_mod, (self.mixer, 0))
|
|
|
|
self.connect(self.lo, (self.mixer, 1))
|
|
self.connect(self.mixer, self.to_real, self)
|
|
|
|
@property
|
|
def tone_freq(self) -> float:
|
|
"""Test tone frequency in Hz."""
|
|
return self._tone_freq
|
|
|
|
@property
|
|
def subcarrier_freq(self) -> float:
|
|
"""Subcarrier center frequency in Hz."""
|
|
return self._subcarrier_freq
|
|
|
|
@property
|
|
def fm_deviation(self) -> float:
|
|
"""FM deviation in Hz."""
|
|
return self._fm_deviation
|
|
|
|
@property
|
|
def audio_input(self) -> bool:
|
|
"""Whether the block accepts external audio input."""
|
|
return self._audio_input
|