""" 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