diff --git a/csdr/chain/analog.py b/csdr/chain/analog.py index 030eb9de..6124665a 100644 --- a/csdr/chain/analog.py +++ b/csdr/chain/analog.py @@ -1,4 +1,5 @@ -from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio, DeemphasisTauChain, MetaProvider +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio, DeemphasisTauChain, \ + MetaProvider, RdsChain from pycsdr.modules import AmDemod, DcBlock, FmDemod, Limit, NfmDeemphasis, Agc, WfmDeemphasis, FractionalDecimator, \ RealPart, Writer, Buffer from pycsdr.types import Format, AgcProfile @@ -42,10 +43,11 @@ class NFm(BaseDemodulatorChain): self.replace(2, NfmDeemphasis(sampleRate)) -class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAudio, MetaProvider): - def __init__(self, sampleRate: int, tau: float): +class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAudio, MetaProvider, RdsChain): + def __init__(self, sampleRate: int, tau: float, rdsRbds: bool): self.sampleRate = sampleRate self.tau = tau + self.rdsRbds = rdsRbds self.limit = Limit() # this buffer is used to tap into the raw audio stream for redsea RDS decoding self.metaTapBuffer = Buffer(Format.FLOAT) @@ -56,6 +58,7 @@ class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAu WfmDeemphasis(self.sampleRate, self.tau), ] self.metaChain = None + self.metaWriter = None super().__init__(workers) def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None: @@ -83,15 +86,25 @@ class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAu if not FeatureDetector().is_available("redsea"): return if self.metaChain is None: - self.metaChain = Redsea(self.getFixedIfSampleRate()) + self.metaChain = Redsea(self.getFixedIfSampleRate(), self.rdsRbds) self.metaChain.setReader(self.metaTapBuffer.getReader()) - self.metaChain.setWriter(writer) + self.metaWriter = writer + self.metaChain.setWriter(self.metaWriter) def stop(self): super().stop() if self.metaChain is not None: self.metaChain.stop() self.metaChain = None + self.metaWriter = None + + def setRdsRbds(self, rdsRbds: bool) -> None: + self.rdsRbds = rdsRbds + if self.metaChain is not None: + self.metaChain.stop() + self.metaChain = Redsea(self.getFixedIfSampleRate(), self.rdsRbds) + self.metaChain.setReader(self.metaTapBuffer.getReader()) + self.metaChain.setWriter(self.metaWriter) class Ssb(BaseDemodulatorChain): diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py index b16f806d..99042d99 100644 --- a/csdr/chain/demodulator.py +++ b/csdr/chain/demodulator.py @@ -49,6 +49,12 @@ class DeemphasisTauChain(ABC): pass +class RdsChain(ABC): + @abstractmethod + def setRdsRbds(self, rdsRbds: bool) -> None: + pass + + class BaseDemodulatorChain(Chain): def supportsSquelch(self) -> bool: return True diff --git a/csdr/chain/redsea.py b/csdr/chain/redsea.py index a2222561..a03da686 100644 --- a/csdr/chain/redsea.py +++ b/csdr/chain/redsea.py @@ -6,9 +6,9 @@ from csdr.module import JsonParser class Redsea(Chain): - def __init__(self, sampleRate: int): + def __init__(self, sampleRate: int, rbds: bool): super().__init__([ Convert(Format.FLOAT, Format.SHORT), - RedseaModule(sampleRate), + RedseaModule(sampleRate, rbds), JsonParser("WFM"), ]) diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py index 7deeed4a..3d7fe6d3 100644 --- a/owrx/config/defaults.py +++ b/owrx/config/defaults.py @@ -17,6 +17,7 @@ defaultConfig = PropertyLayer( audio_compression="adpcm", fft_compression="adpcm", wfm_deemphasis_tau=50e-6, + wfm_rds_rbds=False, digimodes_fft_size=2048, digital_voice_dmr_id_lookup=True, digital_voice_nxdn_id_lookup=True, diff --git a/owrx/controllers/settings/decoding.py b/owrx/controllers/settings/decoding.py index fb0e5424..480b0e9b 100644 --- a/owrx/controllers/settings/decoding.py +++ b/owrx/controllers/settings/decoding.py @@ -32,6 +32,10 @@ class DecodingSettingsController(SettingsFormController): infotext='See this Wikipedia article for more information', ), + CheckboxInput( + "wfm_rds_rbds", + "Enable RBDS decoding (US RDS standard)", + ), ), Section( "Digital voice", diff --git a/owrx/dsp.py b/owrx/dsp.py index fb33fe8f..78d6f13a 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -3,7 +3,7 @@ from owrx.property import PropertyStack, PropertyLayer, PropertyValidator, Prope from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.modes import Modes, DigitalMode from csdr.chain import Chain -from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain, DemodulatorError +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain, DemodulatorError, RdsChain from csdr.chain.selector import Selector, SecondarySelector from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.fft import FftChain @@ -47,6 +47,7 @@ class ClientDemodulatorChain(Chain): self.centerFrequency = None self.frequencyOffset = None self.wfmDeemphasisTau = 50e-6 + self.rdsRbds = False inputRate = demod.getFixedAudioRate() if isinstance(demod, FixedAudioRateChain) else outputRate oRate = hdOutputRate if isinstance(demod, HdAudio) else outputRate self.clientAudioChain = ClientAudioChain(demod.getOutputFormat(), inputRate, oRate, audioCompression) @@ -109,6 +110,9 @@ class ClientDemodulatorChain(Chain): if isinstance(self.demodulator, DeemphasisTauChain): self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) + if isinstance(self.demodulator, RdsChain): + self.demodulator.setRdsRbds(self.rdsRbds) + self._updateDialFrequency() self._syncSquelch() @@ -377,6 +381,13 @@ class ClientDemodulatorChain(Chain): if isinstance(self.demodulator, DeemphasisTauChain): self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) + def setRdsRbds(self, rdsRbds: bool) -> None: + if rdsRbds == self.rdsRbds: + return + self.rdsRbds = rdsRbds + if isinstance(self.demodulator, RdsChain): + self.demodulator.setRdsRbds(self.rdsRbds) + class ModulationValidator(OrValidator): """ @@ -427,6 +438,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) "start_mod", "start_freq", "wfm_deemphasis_tau", + "wfm_rds_rbds", "digital_voice_codecserver", ), ) @@ -491,6 +503,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) self.props.wireProperty("mod", self.setDemodulator), self.props.wireProperty("dmr_filter", self.chain.setSlotFilter), self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau), + self.props.wireProperty("wfm_rds_rbds", self.chain.setRdsRbds), self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator), self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset), ] @@ -532,7 +545,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) return NFm(self.props["output_rate"]) elif demod == "wfm": from csdr.chain.analog import WFm - return WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"]) + return WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"], self.props["wfm_rds_rbds"]) elif demod == "am": from csdr.chain.analog import Am return Am() diff --git a/owrx/rds/redsea.py b/owrx/rds/redsea.py index b9409773..7f1491ad 100644 --- a/owrx/rds/redsea.py +++ b/owrx/rds/redsea.py @@ -3,9 +3,12 @@ from pycsdr.types import Format class RedseaModule(ExecModule): - def __init__(self, sampleRate: int): + def __init__(self, sampleRate: int, rbds: bool): + args = ["redsea", "--input", "mpx", "--samplerate", str(sampleRate)] + if rbds: + args += ["--rbds"] super().__init__( Format.SHORT, Format.CHAR, - ["redsea", "--input", "mpx", "--samplerate", str(sampleRate)] + args )