diff --git a/sats_receiver/gr_modules/decoders.py b/sats_receiver/gr_modules/decoders.py index 9fc913f..4c8bf14 100644 --- a/sats_receiver/gr_modules/decoders.py +++ b/sats_receiver/gr_modules/decoders.py @@ -86,8 +86,20 @@ class RawDecoder(Decoder): def __init__(self, recorder: 'SatRecorder', samp_rate: Union[int, float], - force_nosend_iq=False): - super(RawDecoder, self).__init__(recorder, samp_rate, 'Raw Decoder', utils.Decode.RAW) + force_nosend_iq=False, + iq_in=True): + super(RawDecoder, self).__init__( + recorder, + samp_rate, + 'Raw Decoder', + utils.Decode.RAW, + [ + gr.gr.io_signature(1, 1, gr.gr.sizeof_gr_complex) + if iq_in + else gr.gr.io_signature(1, 1, gr.gr.sizeof_float), + gr.gr.io_signature(0, 0, 0), + ], + ) out_fmt = recorder.raw_out_format out_subfmt = recorder.raw_out_subformat @@ -97,10 +109,18 @@ def __init__(self, out_fmt = utils.RawOutFormat.WAV out_subfmt = utils.RawOutDefaultSub.WAV - self.ctf = gr.blocks.complex_to_float(1) + self.base_kw['out_fmt'] = out_fmt + self.base_kw['iq_in'] = iq_in + + pre_sink = self + if iq_in: + pre_sink = self.ctf = gr.blocks.complex_to_float(1) + self.connect(self, pre_sink) + + ch_n = iq_in and 2 or 1 self.wav_sink = gr.blocks.wavfile_sink( str(self.tmp_file), - 2, + ch_n, samp_rate, out_fmt.value, out_subfmt.value, @@ -109,8 +129,8 @@ def __init__(self, self.wav_sink.close() utils.unlink(self.tmp_file) - self.connect(self, self.ctf, self.wav_sink) - self.connect((self.ctf, 1), (self.wav_sink, 1)) + for ch in range(ch_n): + self.connect((pre_sink, ch), (self.wav_sink, ch)) def start(self): super(RawDecoder, self).start() @@ -132,12 +152,15 @@ def _raw_finalize(log: logging.Logger, observation_key: str, wf_cfg: dict, send_iq: bool, + out_fmt: utils.RawOutFormat, + iq_in: bool, **kw) -> tuple[utils.Decode, str, str, dict[utils.RawFileType, pathlib.Path], dt.datetime]: log.debug('finalizing...') st = tmp_file.stat() d = dt.datetime.fromtimestamp(st.st_mtime, dateutil.tz.tzutc()) - res_fn = tmp_file.rename(out_dir / d.strftime(f'{sat_name}_%Y-%m-%d_%H-%M-%S,%f{subname}_RAW.wav')) + suff = 'ogg' if out_fmt == utils.RawOutFormat.OGG else 'wav' + res_fn = tmp_file.rename(out_dir / d.strftime(f'{sat_name}_%Y-%m-%d_%H-%M-%S,%f{subname}_RAW.{suff}')) files = {} if wf_cfg is not None: @@ -150,7 +173,8 @@ def _raw_finalize(log: logging.Logger, utils.unlink(wfp) if send_iq and st.st_size: - files[utils.RawFileType.IQ] = res_fn + k = utils.RawFileType.IQ if iq_in else utils.RawFileType.AUDIO + files[k] = res_fn else: res_fn.unlink(True) diff --git a/sats_receiver/gr_modules/modules.py b/sats_receiver/gr_modules/modules.py index 4037b8d..1023e4b 100644 --- a/sats_receiver/gr_modules/modules.py +++ b/sats_receiver/gr_modules/modules.py @@ -142,6 +142,7 @@ def __init__(self, self.radio = RadioModule(main_tune, samp_rate, self.bandwidth, self.frequency) self.demodulator = None self.post_demod = gr.blocks.float_to_complex() + self.iq_demod = 1 try: self.mode == self.decode @@ -159,6 +160,7 @@ def __init__(self, audio_pass=5000, audio_stop=5500, ) + self.iq_demod = 0 elif self.mode == utils.Mode.FM: self.demodulator = gr.analog.fm_demod_cf( @@ -170,12 +172,14 @@ def __init__(self, gain=1.0, tau=0, ) + self.iq_demod = 0 elif self.mode == utils.Mode.WFM: self.demodulator = gr.analog.wfm_rcv( quad_rate=self.bandwidth, audio_decimation=1, ) + self.iq_demod = 0 elif self.mode == utils.Mode.WFM_STEREO: if self.bandwidth < 76800: @@ -190,9 +194,11 @@ def __init__(self, elif self.mode == utils.Mode.QUAD: self.demodulator = gr.analog.quadrature_demod_cf(self.quad_gain) + self.iq_demod = 0 elif self.mode == utils.Mode.SSTV_QUAD: self.demodulator = demodulators.SstvQuadDemod(self.bandwidth, self.demode_out_sr, self.quad_gain) + self.iq_demod = 0 elif self.mode in (utils.Mode.QPSK, utils.Mode.OQPSK): oqpsk = self.mode == utils.Mode.OQPSK @@ -201,13 +207,14 @@ def __init__(self, self.post_demod = None elif self.mode in (utils.Mode.FSK, utils.Mode.GFSK, utils.Mode.GMSK): - x = { + fsk_demod = { utils.Mode.FSK: demodulators.FskDemod, utils.Mode.GFSK: demodulators.GfskDemod, utils.Mode.GMSK: demodulators.GmskDemod, } - self.demodulator = x[self.mode](self.bandwidth, self.channels, self.deviation_factor) + self.demodulator = fsk_demod[self.mode](self.bandwidth, self.channels, self.deviation_factor) self.post_demod = None + self.iq_demod = 0 channels = getattr(self.demodulator, 'channels', (self.bandwidth,)) self.decoders = [] @@ -225,8 +232,11 @@ def __init__(self, self.decoders.append(decoders.CcsdsConvConcatDecoder(self, self.bandwidth)) elif self.decode == utils.Decode.RAW: + if not self.iq_demod: + self.post_demod = None + for ch in channels: - self.decoders.append(decoders.RawDecoder(self, ch)) + self.decoders.append(decoders.RawDecoder(self, ch, iq_in=self.iq_demod)) elif self.decode == utils.Decode.SSTV: self.decoders.append(decoders.SstvDecoder(self, self.demode_out_sr)) diff --git a/sats_receiver/utils.py b/sats_receiver/utils.py index 0401ec6..12fc6b7 100644 --- a/sats_receiver/utils.py +++ b/sats_receiver/utils.py @@ -140,6 +140,7 @@ class RawOutDefaultSub(enum.Enum): class RawFileType(enum.StrEnum): IQ = enum.auto() WFC = enum.auto() + AUDIO = enum.auto() class Phase(enum.IntEnum): diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 9bef8b4..625309f 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -23,8 +23,8 @@ from PIL import Image, ExifTags from sats_receiver import utils -from sats_receiver.gr_modules.decoders import Decoder, AptDecoder, CcsdsConvConcatDecoder, ProtoDecoder, SatellitesDecoder, SstvDecoder -from sats_receiver.gr_modules.demodulators import FskDemod +from sats_receiver.gr_modules.decoders import Decoder, AptDecoder, CcsdsConvConcatDecoder, ProtoDecoder, RawDecoder, SatellitesDecoder, SstvDecoder +from sats_receiver.gr_modules.demodulators import FskDemod, SstvQuadDemod from sats_receiver.gr_modules.epb.prober import Prober from sats_receiver.observer import Observer from sats_receiver.systems.apt import Apt @@ -157,7 +157,9 @@ def setUpClass(cls) -> None: 'satellite subname mode ' 'sstv_sync sstv_wsr sstv_live_exec ' 'ccc_pre_deint ccc_frame_size ccc_diff ccc_rs_dualbasis ccc_rs_interleaving ccc_derandomize ' - 'proto_deframer proto_options') + 'proto_deframer proto_options ' + 'raw_out_format raw_out_subformat ' + 'iq_waterfall') cls.out_dir = tempfile.TemporaryDirectory('.d', 'sats-receiver-test-', ignore_cleanup_errors=True) cls.out_dp = pathlib.Path(cls.out_dir.name) @@ -181,7 +183,9 @@ def setUp(self) -> None: self.recorder = self.rec_nt(self.satellite, 'test', utils.Mode.OQPSK, 1, 16000, 0, 1, 892, 1, 0, 4, 1, - utils.ProtoDeframer.USP, {}) + utils.ProtoDeframer.USP, {}, + utils.RawOutFormat.WAV, utils.RawOutSubFormat.FLOAT, + None) def tearDown(self) -> None: if isinstance(self.tb, DecoderTopBlock): @@ -441,9 +445,66 @@ def test_proto(self): self.tb.wait() x = self.tb.executor.action(TIMEOUT) - print(x) self.assertIsInstance(x, tuple) dtype, deftype, sat_name, observation_key, res_filename, end_time = x self.assertEqual(utils.Decode.PROTO, dtype) self.assertEqual(utils.ProtoDeframer.USP, deftype) self.assertRegex(res_filename.name, r'.+\.(kss)') + + def test_raw_channels_1(self): + wav_fp = FILES / 'orbicraft_tlm_4800@16000.wav' + wav_samp_rate = 16000 + out_sr = 8000 + + demod = SstvQuadDemod(wav_samp_rate, out_sr) + decoder = RawDecoder(self.recorder, out_sr, iq_in=0) + self.tb = DecoderTopBlock(2, wav_fp, decoder, self.executor, demod=demod) + self.tb.start() + + while self.tb.prober.changes(): + time.sleep(self.tb.prober.measure_s) + time.sleep(self.tb.prober.measure_s) + + self.tb.stop() + self.tb.wait() + + x = self.tb.executor.action(TIMEOUT) + self.assertIsInstance(x, tuple) + dtype, sat_name, observation_key, files, end_time = x + self.assertEqual(utils.Decode.RAW, dtype) + self.assertEqual(self.recorder.satellite.name, sat_name) + self.assertEqual(1, len(files)) + self.assertIn(utils.RawFileType.AUDIO, files) + + fp = files[utils.RawFileType.AUDIO] + wav = gr.blocks.wavfile_source(str(fp), False) + self.assertEqual(1, wav.channels()) + self.assertEqual(out_sr, wav.sample_rate()) + + def test_raw_channels_2(self): + wav_fp = FILES / 'orbicraft_tlm_4800@16000.wav' + wav_samp_rate = 16000 + + decoder = RawDecoder(self.recorder, wav_samp_rate, iq_in=1) + self.tb = DecoderTopBlock(2, wav_fp, decoder, self.executor) + self.tb.start() + + while self.tb.prober.changes(): + time.sleep(self.tb.prober.measure_s) + time.sleep(self.tb.prober.measure_s) + + self.tb.stop() + self.tb.wait() + + x = self.tb.executor.action(TIMEOUT) + self.assertIsInstance(x, tuple) + dtype, sat_name, observation_key, files, end_time = x + self.assertEqual(utils.Decode.RAW, dtype) + self.assertEqual(self.recorder.satellite.name, sat_name) + self.assertEqual(1, len(files)) + self.assertIn(utils.RawFileType.IQ, files) + + fp = files[utils.RawFileType.IQ] + wav = gr.blocks.wavfile_source(str(fp), False) + self.assertEqual(2, wav.channels()) + self.assertEqual(wav_samp_rate, wav.sample_rate())