Skip to content

Commit

Permalink
#16 WAV output channels sync
Browse files Browse the repository at this point in the history
  • Loading branch information
baskiton committed Jul 23, 2024
1 parent 5819c9f commit 7604bbc
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 16 deletions.
40 changes: 32 additions & 8 deletions sats_receiver/gr_modules/decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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()
Expand All @@ -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:
Expand All @@ -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)

Expand Down
16 changes: 13 additions & 3 deletions sats_receiver/gr_modules/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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 = []
Expand All @@ -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))
Expand Down
1 change: 1 addition & 0 deletions sats_receiver/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
71 changes: 66 additions & 5 deletions tests/test_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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())

0 comments on commit 7604bbc

Please sign in to comment.