diff --git a/csdr/__init__.py b/csdr/__init__.py index e5c70ab..a22be7f 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 0c3b085..7b1e47f 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 74d40a7..0000000 --- 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 0000000..15b8d1b --- /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 b67ace5..39ae8d5 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 8971c79..8ca5556 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 3c14272..fbf2de8 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 9a5f541..6e39d5d 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 f32f454..0000000 --- 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 0000000..2882603 --- /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 9f20368..0000000 --- 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 3347bca..90332cd 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 d601e1c..df0253c 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()