From 5032f4b66dc84e574d77ce228c49ada1b0065eb3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 23 Aug 2021 14:25:28 +0200 Subject: [PATCH] first steps at rewiring the dsp stuff --- csdr/__init__.py | 6 +- csdr/chain/__init__.py | 83 ++++++++++---- csdr/chain/am.py | 17 --- csdr/chain/analog.py | 54 ++++++++++ csdr/chain/clientaudio.py | 24 ++++- csdr/chain/demodulator.py | 67 ++---------- csdr/chain/digiham.py | 15 ++- csdr/chain/fft.py | 4 +- csdr/chain/fm.py | 28 ----- csdr/chain/selector.py | 126 ++++++++++++++++++++++ csdr/chain/ssb.py | 12 --- owrx/connection.py | 3 +- owrx/dsp.py | 220 ++++++++++++++++++++++++++++++-------- 13 files changed, 465 insertions(+), 194 deletions(-) delete mode 100644 csdr/chain/am.py create mode 100644 csdr/chain/analog.py delete mode 100644 csdr/chain/fm.py create mode 100644 csdr/chain/selector.py delete mode 100644 csdr/chain/ssb.py diff --git a/csdr/__init__.py b/csdr/__init__.py index e5c70ab1..a22be7f4 100644 --- a/csdr/__init__.py +++ b/csdr/__init__.py @@ -37,10 +37,8 @@ from csdr.pipe import Pipe from pycsdr.modules import Buffer from pycsdr.types import Format -from csdr.chain.demodulator import DemodulatorChain -from csdr.chain.fm import NFm, WFm -from csdr.chain.am import Am -from csdr.chain.ssb import Ssb +from csdr.chain.selector import Selector +from csdr.chain.analog import Am, NFm, WFm, Ssb from csdr.chain.digiham import Dstar, Nxdn, Dmr, Ysf from csdr.chain.clientaudio import ClientAudioChain diff --git a/csdr/chain/__init__.py b/csdr/chain/__init__.py index 0c3b0856..7b1e47f2 100644 --- a/csdr/chain/__init__.py +++ b/csdr/chain/__init__.py @@ -1,12 +1,13 @@ -from pycsdr.modules import Buffer, Writer +from pycsdr.modules import Buffer +from typing import Union, Callable class Chain: - def __init__(self, *workers): + def __init__(self, workers): self.reader = None self.writer = None self.clientReader = None - self.workers = list(workers) + self.workers = workers for i in range(1, len(self.workers)): self._connect(self.workers[i - 1], self.workers[i]) @@ -29,19 +30,17 @@ class Chain: if self.workers: self.workers[-1].setWriter(writer) - def stop(self): - for w in self.workers: - w.stop() - if self.clientReader is not None: - # TODO should be covered by finalize - self.clientReader.stop() - self.clientReader = None + def indexOf(self, search: Union[Callable, object]) -> int: + def searchFn(x): + if callable(search): + return search(x) + else: + return x is search - def getOutputFormat(self): - if self.workers: - return self.workers[-1].getOutputFormat() - else: - raise BufferError("getOutputFormat on empty chain") + try: + return next(i for i, v in enumerate(self.workers) if searchFn(v)) + except StopIteration: + return -1 def replace(self, index, newWorker): if index >= len(self.workers): @@ -55,18 +54,59 @@ class Chain: newWorker.setReader(self.reader) else: previousWorker = self.workers[index - 1] - buffer = Buffer(previousWorker.getOutputFormat()) - previousWorker.setWriter(buffer) - newWorker.setReader(buffer.getReader()) + self._connect(previousWorker, newWorker) if index == len(self.workers) - 1: if self.writer is not None: newWorker.setWriter(self.writer) else: nextWorker = self.workers[index + 1] - buffer = Buffer(newWorker.getOutputFormat()) - newWorker.setWriter(buffer) - nextWorker.setReader(buffer.getReader()) + self._connect(newWorker, nextWorker) + + def append(self, newWorker): + previousWorker = None + if self.workers: + previousWorker = self.workers[-1] + + self.workers.append(newWorker) + + if previousWorker: + self._connect(previousWorker, newWorker) + elif self.reader is not None: + newWorker.setReader(self.reader) + + if self.writer is not None: + newWorker.setWriter(self.writer) + + def remove(self, index): + removedWorker = self.workers[index] + self.workers.remove(removedWorker) + removedWorker.stop() + + if index == 0: + if self.reader is not None: + self.workers[0].setReader(self.reader) + elif index == len(self.workers): + if self.writer is not None: + self.workers[-1].setWriter(self.writer) + else: + previousWorker = self.workers[index - 1] + nextWorker = self.workers[index] + self._connect(previousWorker, nextWorker) + + def stop(self): + for w in self.workers: + w.stop() + if self.clientReader is not None: + # TODO should be covered by finalize + self.clientReader.stop() + self.clientReader = None + + def getOutputFormat(self): + if self.workers: + return self.workers[-1].getOutputFormat() + else: + raise BufferError("getOutputFormat on empty chain") def pump(self, write): if self.writer is None: @@ -87,4 +127,3 @@ class Chain: write(data) return copy - diff --git a/csdr/chain/am.py b/csdr/chain/am.py deleted file mode 100644 index 74d40a79..00000000 --- a/csdr/chain/am.py +++ /dev/null @@ -1,17 +0,0 @@ -from csdr.chain import Chain -from pycsdr.modules import AmDemod, DcBlock, Agc, Convert -from pycsdr.types import Format, AgcProfile - - -class Am(Chain): - def __init__(self): - agc = Agc(Format.FLOAT) - agc.setProfile(AgcProfile.SLOW) - agc.setInitialGain(200) - workers = [ - AmDemod(), - DcBlock(), - agc, - ] - - super().__init__(*workers) diff --git a/csdr/chain/analog.py b/csdr/chain/analog.py new file mode 100644 index 00000000..15b8d1b7 --- /dev/null +++ b/csdr/chain/analog.py @@ -0,0 +1,54 @@ +from csdr.chain.demodulator import BaseDemodulatorChain +from pycsdr.modules import AmDemod, DcBlock, FmDemod, Limit, NfmDeemphasis, Agc, WfmDeemphasis, FractionalDecimator, RealPart +from pycsdr.types import Format, AgcProfile + + +class Am(BaseDemodulatorChain): + def __init__(self): + agc = Agc(Format.FLOAT) + agc.setProfile(AgcProfile.SLOW) + agc.setInitialGain(200) + workers = [ + AmDemod(), + DcBlock(), + agc, + ] + + super().__init__(workers) + + +class NFm(BaseDemodulatorChain): + def __init__(self, sampleRate: int): + agc = Agc(Format.FLOAT) + agc.setProfile(AgcProfile.SLOW) + agc.setMaxGain(3) + workers = [ + FmDemod(), + Limit(), + NfmDeemphasis(sampleRate), + agc, + ] + super().__init__(workers) + + +class WFm(BaseDemodulatorChain): + def __init__(self, sampleRate: int, tau: float): + workers = [ + FmDemod(), + Limit(), + FractionalDecimator(Format.FLOAT, 200000.0 / sampleRate, prefilter=True), + WfmDeemphasis(sampleRate, tau), + ] + super().__init__(workers) + + def getFixedIfSampleRate(self): + return 200000 + + +class Ssb(BaseDemodulatorChain): + def __init__(self): + workers = [ + RealPart(), + Agc(Format.FLOAT), + ] + super().__init__(workers) diff --git a/csdr/chain/clientaudio.py b/csdr/chain/clientaudio.py index b67ace5b..39ae8d58 100644 --- a/csdr/chain/clientaudio.py +++ b/csdr/chain/clientaudio.py @@ -6,6 +6,8 @@ from pycsdr.types import Format class ClientAudioChain(Chain): def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str): workers = [] + self.inputRate = inputRate + self.clientRate = clientRate if inputRate != clientRate: # we only have an audio resampler for float ATM so if we need to resample, we need to convert if format != Format.FLOAT: @@ -15,4 +17,24 @@ class ClientAudioChain(Chain): workers += [Convert(format, Format.SHORT)] if compression == "adpcm": workers += [AdpcmEncoder(sync=True)] - super().__init__(*workers) + super().__init__(workers) + + def setFormat(self, format: Format) -> None: + pass + + def setInputRate(self, inputRate: int) -> None: + if inputRate == self.inputRate: + return + + def setClientRate(self, clientRate: int) -> None: + if clientRate == self.clientRate: + return + + def setAudioCompression(self, compression: str) -> None: + index = self.indexOf(lambda x: isinstance(x, AdpcmEncoder)) + if compression == "adpcm": + if index < 0: + self.append(AdpcmEncoder(sync=True)) + else: + if index >= 0: + self.remove(index) diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py index 8971c790..8ca5556b 100644 --- a/csdr/chain/demodulator.py +++ b/csdr/chain/demodulator.py @@ -1,65 +1,12 @@ from csdr.chain import Chain -from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer -from pycsdr.types import Format -from csdr.chain.digiham import Dmr -class DemodulatorChain(Chain): - def __init__(self, samp_rate: int, audioRate: int, shiftRate: float, demodulator: Chain): - self.demodulator = demodulator +class BaseDemodulatorChain(Chain): + def getFixedIfSampleRate(self): + return None - self.shift = Shift(shiftRate) + def getFixedAudioRate(self): + return None - decimation, fraction = self._getDecimation(samp_rate, audioRate) - if_samp_rate = samp_rate / decimation - transition = 0.15 * (if_samp_rate / float(samp_rate)) - # set the cutoff on the fist decimation stage lower so that the resulting output - # is already prepared for the second (fractional) decimation stage. - # this spares us a second filter. - self.decimation = FirDecimate(decimation, transition, 0.5 * decimation / (samp_rate / audioRate)) - - bp_transition = 320.0 / audioRate - self.bandpass = Bandpass(transition=bp_transition, use_fft=True) - - readings_per_second = 4 - # s-meter readings are available every 1024 samples - # the reporting interval is measured in those 1024-sample blocks - self.squelch = Squelch(5, int(audioRate / (readings_per_second * 1024))) - - workers = [self.shift, self.decimation] - - if fraction != 1.0: - workers += [FractionalDecimator(Format.COMPLEX_FLOAT, fraction)] - - workers += [self.bandpass, self.squelch, demodulator] - - super().__init__(*workers) - - def setShiftRate(self, rate: float): - self.shift.setRate(rate) - - def setSquelchLevel(self, level: float): - self.squelch.setSquelchLevel(level) - - def setBandpass(self, low_cut: float, high_cut: float): - self.bandpass.setBandpass(low_cut, high_cut) - - def setPowerWriter(self, writer: Writer): - self.squelch.setPowerWriter(writer) - - def setMetaWriter(self, writer: Writer): - self.demodulator.setMetaWriter(writer) - - def setDmrFilter(self, filter: int) -> None: - if isinstance(self.demodulator, Dmr): - self.demodulator.setSlotFilter(filter) - - def _getDecimation(self, input_rate, output_rate): - if output_rate <= 0: - raise ValueError("invalid output rate: {rate}".format(rate=output_rate)) - decimation = 1 - target_rate = output_rate - while input_rate / (decimation + 1) >= target_rate: - decimation += 1 - fraction = float(input_rate / decimation) / output_rate - return decimation, fraction + def supportsSquelch(self): + return True diff --git a/csdr/chain/digiham.py b/csdr/chain/digiham.py index 3c14272b..fbf2de88 100644 --- a/csdr/chain/digiham.py +++ b/csdr/chain/digiham.py @@ -1,11 +1,11 @@ -from csdr.chain import Chain +from csdr.chain.demodulator import BaseDemodulatorChain from pycsdr.modules import FmDemod, Agc, Writer from pycsdr.types import Format from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder from digiham.ambe import Modes -class DigihamChain(Chain): +class DigihamChain(BaseDemodulatorChain): def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""): self.decoder = decoder if codecserver is None: @@ -23,11 +23,20 @@ class DigihamChain(Chain): DigitalVoiceFilter(), agc ] - super().__init__(*workers) + super().__init__(workers) + + def getFixedIfSampleRate(self): + return 48000 + + def getFixedAudioRate(self): + return 8000 def setMetaWriter(self, writer: Writer): self.decoder.setMetaWriter(writer) + def supportsSquelch(self): + return False + class Dstar(DigihamChain): def __init__(self, codecserver: str = ""): diff --git a/csdr/chain/fft.py b/csdr/chain/fft.py index 9a5f541f..6e39d5d1 100644 --- a/csdr/chain/fft.py +++ b/csdr/chain/fft.py @@ -7,7 +7,7 @@ class FftAverager(Chain): self.fftSize = fft_size self.fftAverages = fft_averages workers = [self._getWorker()] - super().__init__(*workers) + super().__init__(workers) def setFftAverages(self, fft_averages): if self.fftAverages == fft_averages: @@ -46,7 +46,7 @@ class FftChain(Chain): self._updateParameters() - super().__init__(*workers) + super().__init__(workers) def _setBlockSize(self, fft_block_size): if self.blockSize == int(fft_block_size): diff --git a/csdr/chain/fm.py b/csdr/chain/fm.py deleted file mode 100644 index f32f4546..00000000 --- a/csdr/chain/fm.py +++ /dev/null @@ -1,28 +0,0 @@ -from csdr.chain import Chain -from pycsdr.modules import FmDemod, Limit, NfmDeemphasis, Agc, WfmDeemphasis, FractionalDecimator -from pycsdr.types import Format, AgcProfile - - -class NFm(Chain): - def __init__(self, sampleRate: int): - agc = Agc(Format.FLOAT) - agc.setProfile(AgcProfile.SLOW) - agc.setMaxGain(3) - workers = [ - FmDemod(), - Limit(), - NfmDeemphasis(sampleRate), - agc, - ] - super().__init__(*workers) - - -class WFm(Chain): - def __init__(self, sampleRate: int, tau: float): - workers = [ - FmDemod(), - Limit(), - FractionalDecimator(Format.FLOAT, 200000.0 / sampleRate, prefilter=True), - WfmDeemphasis(sampleRate, tau), - ] - super().__init__(*workers) diff --git a/csdr/chain/selector.py b/csdr/chain/selector.py new file mode 100644 index 00000000..2882603d --- /dev/null +++ b/csdr/chain/selector.py @@ -0,0 +1,126 @@ +from csdr.chain import Chain +from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer +from pycsdr.types import Format +import math + + +class Decimator(Chain): + def __init__(self, inputRate: int, outputRate: int): + if outputRate > inputRate: + raise ValueError("impossible decimation: cannot upsample {} to {}".format(inputRate, outputRate)) + self.inputRate = inputRate + self.outputRate = outputRate + + decimation, fraction = self._getDecimation(outputRate) + transition = 0.15 * (outputRate / float(self.inputRate)) + # set the cutoff on the fist decimation stage lower so that the resulting output + # is already prepared for the second (fractional) decimation stage. + # this spares us a second filter. + cutoff = 0.5 * decimation / (self.inputRate / outputRate) + + workers = [ + FirDecimate(decimation, transition, cutoff), + ] + + if fraction != 1.0: + workers += [FractionalDecimator(Format.COMPLEX_FLOAT, fraction)] + + super().__init__(workers) + + def _getDecimation(self, outputRate: int) -> (int, float): + d = self.inputRate / outputRate + dInt = int(d) + dFloat = float(self.inputRate / dInt) / outputRate + return dInt, dFloat + + def _reconfigure(self): + decimation, fraction = self._getDecimation(self.outputRate) + transition = 0.15 * (self.outputRate / float(self.inputRate)) + cutoff = 0.5 * decimation / (self.inputRate / self.outputRate) + self.replace(0, FirDecimate(decimation, transition, cutoff)) + index = self.indexOf(lambda x: isinstance(x, FractionalDecimator)) + if fraction != 1.0: + decimator = FractionalDecimator(Format.COMPLEX_FLOAT, fraction) + if index >= 0: + self.replace(index, decimator) + else: + self.append(decimator) + elif index >= 0: + self.remove(index) + + def setOutputRate(self, outputRate: int) -> None: + if outputRate == self.outputRate: + return + self.outputRate = outputRate + self._reconfigure() + + def setInputRate(self, inputRate: int) -> None: + if inputRate == self.inputRate: + return + self.inputRate = inputRate + self._reconfigure() + + +class Selector(Chain): + def __init__(self, inputRate: int, outputRate: int, shiftRate: float): + self.outputRate = outputRate + + self.shift = Shift(shiftRate) + + self.decimation = Decimator(inputRate, outputRate) + + self.bandpass = self._buildBandpass() + self.bandpassCutoffs = None + self.setBandpass(-4000, 4000) + + self.readings_per_second = 4 + # s-meter readings are available every 1024 samples + # the reporting interval is measured in those 1024-sample blocks + self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024))) + + workers = [self.shift, self.decimation, self.bandpass, self.squelch] + + super().__init__(workers) + + def _buildBandpass(self) -> Bandpass: + bp_transition = 320.0 / self.outputRate + return Bandpass(transition=bp_transition, use_fft=True) + + def setShiftRate(self, rate: float) -> None: + self.shift.setRate(rate) + + def _convertToLinear(self, db: float) -> float: + return float(math.pow(10, db / 10)) + + def setSquelchLevel(self, level: float) -> None: + self.squelch.setSquelchLevel(self._convertToLinear(level)) + + def setBandpass(self, lowCut: float, highCut: float) -> None: + self.bandpassCutoffs = [lowCut, highCut] + scaled = [x / self.outputRate for x in self.bandpassCutoffs] + self.bandpass.setBandpass(*scaled) + + def setLowCut(self, lowCut: float) -> None: + self.bandpassCutoffs[0] = lowCut + self.setBandpass(*self.bandpassCutoffs) + + def setHighCut(self, highCut: float) -> None: + self.bandpassCutoffs[1] = highCut + self.setBandpass(*self.bandpassCutoffs) + + def setPowerWriter(self, writer: Writer) -> None: + self.squelch.setPowerWriter(writer) + + def setOutputRate(self, outputRate: int) -> None: + if outputRate == self.outputRate: + return + self.outputRate = outputRate + + self.decimation.setOutputRate(outputRate) + self.squelch.setReportInterval(int(outputRate / (self.readings_per_second * 1024))) + self.bandpass = self._buildBandpass() + self.setBandpass(*self.bandpassCutoffs) + self.replace(2, self.bandpass) + + def setInputRate(self, inputRate: int) -> None: + self.decimation.setInputRate(inputRate) diff --git a/csdr/chain/ssb.py b/csdr/chain/ssb.py deleted file mode 100644 index 9f203682..00000000 --- a/csdr/chain/ssb.py +++ /dev/null @@ -1,12 +0,0 @@ -from csdr.chain import Chain -from pycsdr.modules import RealPart, Agc, Convert -from pycsdr.types import Format - - -class Ssb(Chain): - def __init__(self): - workers = [ - RealPart(), - Agc(Format.FLOAT), - ] - super().__init__(*workers) diff --git a/owrx/connection.py b/owrx/connection.py index 3347bca6..90332cd0 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -378,7 +378,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): def write_s_meter_level(self, level): if isinstance(level, memoryview): - level, = struct.unpack('f', level) + # may contain more than one sample, so only take the last 4 bytes = 1 float + level, = struct.unpack('f', level[-4:]) if not isinstance(level, float): logger.warning("s-meter value has unexpected type") return diff --git a/owrx/dsp.py b/owrx/dsp.py index d601e1cd..df0253cb 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -9,7 +9,15 @@ from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.modes import Modes from owrx.config.core import CoreConfig from csdr.output import Output -from csdr import Dsp +from csdr.chain import Chain +from csdr.chain.demodulator import BaseDemodulatorChain +from csdr.chain.selector import Selector +from csdr.chain.clientaudio import ClientAudioChain +from csdr.chain.analog import NFm, WFm, Am, Ssb +from csdr.chain.digiham import Dmr, Dstar, Nxdn, Ysf +from pycsdr.modules import Buffer, Writer +from pycsdr.types import Format +from typing import Union import threading import re @@ -18,6 +26,82 @@ import logging logger = logging.getLogger(__name__) +class ClientDemodulatorChain(Chain): + def __init__(self, demod: BaseDemodulatorChain, sampleRate: int, outputRate: int, audioCompression: str): + self.sampleRate = sampleRate + self.outputRate = outputRate + self.selector = Selector(sampleRate, outputRate, 0.0) + self.selector.setBandpass(-4000, 4000) + self.demodulator = demod + self.clientAudioChain = ClientAudioChain(Format.FLOAT, outputRate, outputRate, audioCompression) + super().__init__([self.selector, self.demodulator, self.clientAudioChain]) + + def setDemodulator(self, demodulator: BaseDemodulatorChain): + self.replace(1, demodulator) + + if self.demodulator is not None: + self.demodulator.stop() + + self.demodulator = demodulator + + ifRate = self.demodulator.getFixedIfSampleRate() + if ifRate is not None: + self.selector.setOutputRate(ifRate) + else: + self.selector.setOutputRate(self.outputRate) + + audioRate = self.demodulator.getFixedAudioRate() + if audioRate is not None: + self.clientAudioChain.setInputRate(audioRate) + else: + self.clientAudioChain.setInputRate(self.outputRate) + + if not demodulator.supportsSquelch(): + self.selector.setSquelchLevel(-150) + + self.clientAudioChain.setFormat(demodulator.getOutputFormat()) + + def setLowCut(self, lowCut): + self.selector.setLowCut(lowCut) + + def setHighCut(self, highCut): + self.selector.setHighCut(highCut) + + def setBandpass(self, lowCut, highCut): + self.selector.setBandpass(lowCut, highCut) + + def setFrequencyOffset(self, offset: int) -> None: + shift = -offset / self.sampleRate + self.selector.setShiftRate(shift) + + def setAudioCompression(self, compression: str) -> None: + self.clientAudioChain.setAudioCompression(compression) + + def setSquelchLevel(self, level: float) -> None: + if not self.demodulator.supportsSquelch(): + return + self.selector.setSquelchLevel(level) + + def setOutputRate(self, outputRate) -> None: + if outputRate == self.outputRate: + return + + self.outputRate = outputRate + if self.demodulator.getFixedIfSampleRate() is None: + self.selector.setOutputRate(outputRate) + if self.demodulator.getFixedAudioRate() is None: + self.clientAudioChain.setClientRate(outputRate) + + def setPowerWriter(self, writer: Writer) -> None: + self.selector.setPowerWriter(writer) + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + self.selector.setInputRate(sampleRate) + + class ModulationValidator(OrValidator): """ This validator only allows alphanumeric characters and numbers, but no spaces or special characters @@ -75,18 +159,28 @@ class DspManager(Output, SdrSourceEventClient): ), ) - self.dsp = Dsp(self) - self.dsp.nc_port = self.sdrSource.getPort() + # TODO wait for the rate to come from the client + if "output_rate" not in self.props: + self.props["output_rate"] = 12000 - def set_low_cut(cut): - bpf = self.dsp.get_bpf() - bpf[0] = cut - self.dsp.set_bpf(*bpf) + self.chain = ClientDemodulatorChain( + self._getDemodulator("nfm"), + self.props["samp_rate"], + self.props["output_rate"], + self.props["audio_compression"] + ) - def set_high_cut(cut): - bpf = self.dsp.get_bpf() - bpf[1] = cut - self.dsp.set_bpf(*bpf) + # wire audio output + buffer = Buffer(self.chain.getOutputFormat()) + self.chain.setWriter(buffer) + reader = buffer.getReader() + self.send_output("audio", reader.read) + + # wire power level output + buffer = Buffer(Format.FLOAT) + self.chain.setPowerWriter(buffer) + reader = buffer.getReader() + self.send_output("smeter", reader.read) def set_dial_freq(changes): if ( @@ -101,39 +195,46 @@ class DspManager(Output, SdrSourceEventClient): parser.setDialFrequency(freq) if "start_mod" in self.props: - self.dsp.set_demodulator(self.props["start_mod"]) + self.setDemodulator(self.props["start_mod"]) mode = Modes.findByModulation(self.props["start_mod"]) if mode and mode.bandpass: - self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut) - else: - self.dsp.set_bpf(-4000, 4000) + bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut] + self.chain.setBandpass(*bpf) if "start_freq" in self.props and "center_freq" in self.props: - self.dsp.set_offset_freq(self.props["start_freq"] - self.props["center_freq"]) + self.chain.setFrequencyOffset(self.props["start_freq"] - self.props["center_freq"]) else: - self.dsp.set_offset_freq(0) + self.chain.setFrequencyOffset(0) self.subscriptions = [ - self.props.wireProperty("audio_compression", self.dsp.set_audio_compression), - self.props.wireProperty("fft_compression", self.dsp.set_fft_compression), - self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size), - self.props.wireProperty("samp_rate", self.dsp.set_samp_rate), - self.props.wireProperty("output_rate", self.dsp.set_output_rate), - self.props.wireProperty("hd_output_rate", self.dsp.set_hd_output_rate), - self.props.wireProperty("offset_freq", self.dsp.set_offset_freq), - self.props.wireProperty("center_freq", self.dsp.set_center_freq), - self.props.wireProperty("squelch_level", self.dsp.set_squelch_level), - self.props.wireProperty("low_cut", set_low_cut), - self.props.wireProperty("high_cut", set_high_cut), - self.props.wireProperty("mod", self.dsp.set_demodulator), - self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter), - self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau), - self.props.wireProperty("digital_voice_codecserver", self.dsp.set_codecserver), + self.props.wireProperty("audio_compression", self.chain.setAudioCompression), + # probably unused: + # self.props.wireProperty("fft_compression", self.dsp.set_fft_compression), + # TODO + # self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size), + self.props.wireProperty("samp_rate", self.chain.setSampleRate), + self.props.wireProperty("output_rate", self.chain.setOutputRate), + # TODO + # self.props.wireProperty("hd_output_rate", self.dsp.set_hd_output_rate), + self.props.wireProperty("offset_freq", self.chain.setFrequencyOffset), + # TODO check, this was used for wsjt-x + # self.props.wireProperty("center_freq", self.dsp.set_center_freq), + self.props.wireProperty("squelch_level", self.chain.setSquelchLevel), + self.props.wireProperty("low_cut", self.chain.setLowCut), + self.props.wireProperty("high_cut", self.chain.setHighCut), + self.props.wireProperty("mod", self.setDemodulator), + # TODO + # self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter), + # TODO + # self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau), + # TODO + # self.props.wireProperty("digital_voice_codecserver", self.dsp.set_codecserver), self.props.filter("center_freq", "offset_freq").wire(set_dial_freq), ] - self.dsp.set_temporary_directory(CoreConfig().get_temporary_directory()) + # TODO + # sp.set_temporary_directory(CoreConfig().get_temporary_directory()) def send_secondary_config(*args): self.handler.write_secondary_dsp_config( @@ -152,9 +253,10 @@ class DspManager(Output, SdrSourceEventClient): send_secondary_config() self.subscriptions += [ - self.props.wireProperty("secondary_mod", set_secondary_mod), - self.props.wireProperty("digimodes_fft_size", send_secondary_config), - self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq), + # TODO + # self.props.wireProperty("secondary_mod", set_secondary_mod), + # self.props.wireProperty("digimodes_fft_size", send_secondary_config), + # self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq), ] self.startOnAvailable = False @@ -163,10 +265,39 @@ class DspManager(Output, SdrSourceEventClient): super().__init__() + def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]): + if isinstance(demod, BaseDemodulatorChain): + return demod + # TODO: move this to Modes + demodChain = None + if demod == "nfm": + demodChain = NFm(self.props["output_rate"]) + elif demod == "wfm": + demodChain = WFm(self.props["output_rate"], self.props["wfm_deemphasis_tau"]) + elif demod == "am": + demodChain = Am() + elif demod in ["usb", "lsb", "cw"]: + demodChain = Ssb() + elif demod == "dmr": + demodChain = Dmr(self.props["digital_voice_codecserver"]) + elif demod == "dstar": + demodChain = Dstar(self.props["digital_voice_codecserver"]) + elif demod == "ysf": + demodChain = Ysf(self.props["digital_voice_codecserver"]) + elif demod == "nxdn": + demodChain = Nxdn(self.props["digital_voice_codecserver"]) + + return demodChain + + def setDemodulator(self, mod): + demodulator = self._getDemodulator(mod) + if demodulator is None: + raise ValueError("unsupported demodulator: {}".format(mod)) + self.chain.setDemodulator(demodulator) + def start(self): if self.sdrSource.isAvailable(): - self.dsp.setBuffer(self.sdrSource.getBuffer()) - self.dsp.start() + self.chain.setReader(self.sdrSource.getBuffer().getReader()) else: self.startOnAvailable = True @@ -187,7 +318,9 @@ class DspManager(Output, SdrSourceEventClient): threading.Thread(target=self.pump(read_fn, write), name="dsp_pump_{}".format(t)).start() def stop(self): - self.dsp.stop() + self.chain.stop() + self.chain = None + self.startOnAvailable = False self.sdrSource.removeClient(self) for sub in self.subscriptions: @@ -208,16 +341,15 @@ class DspManager(Output, SdrSourceEventClient): if state is SdrSourceState.RUNNING: logger.debug("received STATE_RUNNING, attempting DspSource restart") if self.startOnAvailable: - self.dsp.setBuffer(self.sdrSource.getBuffer()) - self.dsp.start() + self.chain.setReader(self.sdrSource.getBuffer().getReader()) self.startOnAvailable = False elif state is SdrSourceState.STOPPING: logger.debug("received STATE_STOPPING, shutting down DspSource") - self.dsp.stop() + self.stop() def onFail(self): logger.debug("received onFail(), shutting down DspSource") - self.dsp.stop() + self.stop() def onShutdown(self): - self.dsp.stop() + self.stop()