first steps at rewiring the dsp stuff

This commit is contained in:
Jakob Ketterl 2021-08-23 14:25:28 +02:00
parent 0f1feb9d47
commit 5032f4b66d
13 changed files with 465 additions and 194 deletions

View File

@ -37,10 +37,8 @@ from csdr.pipe import Pipe
from pycsdr.modules import Buffer from pycsdr.modules import Buffer
from pycsdr.types import Format from pycsdr.types import Format
from csdr.chain.demodulator import DemodulatorChain from csdr.chain.selector import Selector
from csdr.chain.fm import NFm, WFm from csdr.chain.analog import Am, NFm, WFm, Ssb
from csdr.chain.am import Am
from csdr.chain.ssb import Ssb
from csdr.chain.digiham import Dstar, Nxdn, Dmr, Ysf from csdr.chain.digiham import Dstar, Nxdn, Dmr, Ysf
from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.clientaudio import ClientAudioChain

View File

@ -1,12 +1,13 @@
from pycsdr.modules import Buffer, Writer from pycsdr.modules import Buffer
from typing import Union, Callable
class Chain: class Chain:
def __init__(self, *workers): def __init__(self, workers):
self.reader = None self.reader = None
self.writer = None self.writer = None
self.clientReader = None self.clientReader = None
self.workers = list(workers) self.workers = workers
for i in range(1, len(self.workers)): for i in range(1, len(self.workers)):
self._connect(self.workers[i - 1], self.workers[i]) self._connect(self.workers[i - 1], self.workers[i])
@ -29,19 +30,17 @@ class Chain:
if self.workers: if self.workers:
self.workers[-1].setWriter(writer) self.workers[-1].setWriter(writer)
def stop(self): def indexOf(self, search: Union[Callable, object]) -> int:
for w in self.workers: def searchFn(x):
w.stop() if callable(search):
if self.clientReader is not None: return search(x)
# TODO should be covered by finalize else:
self.clientReader.stop() return x is search
self.clientReader = None
def getOutputFormat(self): try:
if self.workers: return next(i for i, v in enumerate(self.workers) if searchFn(v))
return self.workers[-1].getOutputFormat() except StopIteration:
else: return -1
raise BufferError("getOutputFormat on empty chain")
def replace(self, index, newWorker): def replace(self, index, newWorker):
if index >= len(self.workers): if index >= len(self.workers):
@ -55,18 +54,59 @@ class Chain:
newWorker.setReader(self.reader) newWorker.setReader(self.reader)
else: else:
previousWorker = self.workers[index - 1] previousWorker = self.workers[index - 1]
buffer = Buffer(previousWorker.getOutputFormat()) self._connect(previousWorker, newWorker)
previousWorker.setWriter(buffer)
newWorker.setReader(buffer.getReader())
if index == len(self.workers) - 1: if index == len(self.workers) - 1:
if self.writer is not None: if self.writer is not None:
newWorker.setWriter(self.writer) newWorker.setWriter(self.writer)
else: else:
nextWorker = self.workers[index + 1] nextWorker = self.workers[index + 1]
buffer = Buffer(newWorker.getOutputFormat()) self._connect(newWorker, nextWorker)
newWorker.setWriter(buffer)
nextWorker.setReader(buffer.getReader()) 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): def pump(self, write):
if self.writer is None: if self.writer is None:
@ -87,4 +127,3 @@ class Chain:
write(data) write(data)
return copy return copy

View File

@ -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)

54
csdr/chain/analog.py Normal file
View File

@ -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)

View File

@ -6,6 +6,8 @@ from pycsdr.types import Format
class ClientAudioChain(Chain): class ClientAudioChain(Chain):
def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str): def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str):
workers = [] workers = []
self.inputRate = inputRate
self.clientRate = clientRate
if inputRate != clientRate: if inputRate != clientRate:
# we only have an audio resampler for float ATM so if we need to resample, we need to convert # we only have an audio resampler for float ATM so if we need to resample, we need to convert
if format != Format.FLOAT: if format != Format.FLOAT:
@ -15,4 +17,24 @@ class ClientAudioChain(Chain):
workers += [Convert(format, Format.SHORT)] workers += [Convert(format, Format.SHORT)]
if compression == "adpcm": if compression == "adpcm":
workers += [AdpcmEncoder(sync=True)] 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)

View File

@ -1,65 +1,12 @@
from csdr.chain import Chain 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): class BaseDemodulatorChain(Chain):
def __init__(self, samp_rate: int, audioRate: int, shiftRate: float, demodulator: Chain): def getFixedIfSampleRate(self):
self.demodulator = demodulator return None
self.shift = Shift(shiftRate) def getFixedAudioRate(self):
return None
decimation, fraction = self._getDecimation(samp_rate, audioRate) def supportsSquelch(self):
if_samp_rate = samp_rate / decimation return True
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

View File

@ -1,11 +1,11 @@
from csdr.chain import Chain from csdr.chain.demodulator import BaseDemodulatorChain
from pycsdr.modules import FmDemod, Agc, Writer from pycsdr.modules import FmDemod, Agc, Writer
from pycsdr.types import Format from pycsdr.types import Format
from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder
from digiham.ambe import Modes from digiham.ambe import Modes
class DigihamChain(Chain): class DigihamChain(BaseDemodulatorChain):
def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""): def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""):
self.decoder = decoder self.decoder = decoder
if codecserver is None: if codecserver is None:
@ -23,11 +23,20 @@ class DigihamChain(Chain):
DigitalVoiceFilter(), DigitalVoiceFilter(),
agc agc
] ]
super().__init__(*workers) super().__init__(workers)
def getFixedIfSampleRate(self):
return 48000
def getFixedAudioRate(self):
return 8000
def setMetaWriter(self, writer: Writer): def setMetaWriter(self, writer: Writer):
self.decoder.setMetaWriter(writer) self.decoder.setMetaWriter(writer)
def supportsSquelch(self):
return False
class Dstar(DigihamChain): class Dstar(DigihamChain):
def __init__(self, codecserver: str = ""): def __init__(self, codecserver: str = ""):

View File

@ -7,7 +7,7 @@ class FftAverager(Chain):
self.fftSize = fft_size self.fftSize = fft_size
self.fftAverages = fft_averages self.fftAverages = fft_averages
workers = [self._getWorker()] workers = [self._getWorker()]
super().__init__(*workers) super().__init__(workers)
def setFftAverages(self, fft_averages): def setFftAverages(self, fft_averages):
if self.fftAverages == fft_averages: if self.fftAverages == fft_averages:
@ -46,7 +46,7 @@ class FftChain(Chain):
self._updateParameters() self._updateParameters()
super().__init__(*workers) super().__init__(workers)
def _setBlockSize(self, fft_block_size): def _setBlockSize(self, fft_block_size):
if self.blockSize == int(fft_block_size): if self.blockSize == int(fft_block_size):

View File

@ -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)

126
csdr/chain/selector.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -378,7 +378,8 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_s_meter_level(self, level): def write_s_meter_level(self, level):
if isinstance(level, memoryview): 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): if not isinstance(level, float):
logger.warning("s-meter value has unexpected type") logger.warning("s-meter value has unexpected type")
return return

View File

@ -9,7 +9,15 @@ from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
from owrx.modes import Modes from owrx.modes import Modes
from owrx.config.core import CoreConfig from owrx.config.core import CoreConfig
from csdr.output import Output 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 threading
import re import re
@ -18,6 +26,82 @@ import logging
logger = logging.getLogger(__name__) 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): class ModulationValidator(OrValidator):
""" """
This validator only allows alphanumeric characters and numbers, but no spaces or special characters 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) # TODO wait for the rate to come from the client
self.dsp.nc_port = self.sdrSource.getPort() if "output_rate" not in self.props:
self.props["output_rate"] = 12000
def set_low_cut(cut): self.chain = ClientDemodulatorChain(
bpf = self.dsp.get_bpf() self._getDemodulator("nfm"),
bpf[0] = cut self.props["samp_rate"],
self.dsp.set_bpf(*bpf) self.props["output_rate"],
self.props["audio_compression"]
)
def set_high_cut(cut): # wire audio output
bpf = self.dsp.get_bpf() buffer = Buffer(self.chain.getOutputFormat())
bpf[1] = cut self.chain.setWriter(buffer)
self.dsp.set_bpf(*bpf) 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): def set_dial_freq(changes):
if ( if (
@ -101,39 +195,46 @@ class DspManager(Output, SdrSourceEventClient):
parser.setDialFrequency(freq) parser.setDialFrequency(freq)
if "start_mod" in self.props: 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"]) mode = Modes.findByModulation(self.props["start_mod"])
if mode and mode.bandpass: if mode and mode.bandpass:
self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut) bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
else: self.chain.setBandpass(*bpf)
self.dsp.set_bpf(-4000, 4000)
if "start_freq" in self.props and "center_freq" in self.props: 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: else:
self.dsp.set_offset_freq(0) self.chain.setFrequencyOffset(0)
self.subscriptions = [ self.subscriptions = [
self.props.wireProperty("audio_compression", self.dsp.set_audio_compression), self.props.wireProperty("audio_compression", self.chain.setAudioCompression),
self.props.wireProperty("fft_compression", self.dsp.set_fft_compression), # probably unused:
self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size), # self.props.wireProperty("fft_compression", self.dsp.set_fft_compression),
self.props.wireProperty("samp_rate", self.dsp.set_samp_rate), # TODO
self.props.wireProperty("output_rate", self.dsp.set_output_rate), # self.props.wireProperty("digimodes_fft_size", self.dsp.set_secondary_fft_size),
self.props.wireProperty("hd_output_rate", self.dsp.set_hd_output_rate), self.props.wireProperty("samp_rate", self.chain.setSampleRate),
self.props.wireProperty("offset_freq", self.dsp.set_offset_freq), self.props.wireProperty("output_rate", self.chain.setOutputRate),
self.props.wireProperty("center_freq", self.dsp.set_center_freq), # TODO
self.props.wireProperty("squelch_level", self.dsp.set_squelch_level), # self.props.wireProperty("hd_output_rate", self.dsp.set_hd_output_rate),
self.props.wireProperty("low_cut", set_low_cut), self.props.wireProperty("offset_freq", self.chain.setFrequencyOffset),
self.props.wireProperty("high_cut", set_high_cut), # TODO check, this was used for wsjt-x
self.props.wireProperty("mod", self.dsp.set_demodulator), # self.props.wireProperty("center_freq", self.dsp.set_center_freq),
self.props.wireProperty("dmr_filter", self.dsp.set_dmr_filter), self.props.wireProperty("squelch_level", self.chain.setSquelchLevel),
self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau), self.props.wireProperty("low_cut", self.chain.setLowCut),
self.props.wireProperty("digital_voice_codecserver", self.dsp.set_codecserver), 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.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): def send_secondary_config(*args):
self.handler.write_secondary_dsp_config( self.handler.write_secondary_dsp_config(
@ -152,9 +253,10 @@ class DspManager(Output, SdrSourceEventClient):
send_secondary_config() send_secondary_config()
self.subscriptions += [ self.subscriptions += [
self.props.wireProperty("secondary_mod", set_secondary_mod), # TODO
self.props.wireProperty("digimodes_fft_size", send_secondary_config), # self.props.wireProperty("secondary_mod", set_secondary_mod),
self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq), # self.props.wireProperty("digimodes_fft_size", send_secondary_config),
# self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
] ]
self.startOnAvailable = False self.startOnAvailable = False
@ -163,10 +265,39 @@ class DspManager(Output, SdrSourceEventClient):
super().__init__() 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): def start(self):
if self.sdrSource.isAvailable(): if self.sdrSource.isAvailable():
self.dsp.setBuffer(self.sdrSource.getBuffer()) self.chain.setReader(self.sdrSource.getBuffer().getReader())
self.dsp.start()
else: else:
self.startOnAvailable = True 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() threading.Thread(target=self.pump(read_fn, write), name="dsp_pump_{}".format(t)).start()
def stop(self): def stop(self):
self.dsp.stop() self.chain.stop()
self.chain = None
self.startOnAvailable = False self.startOnAvailable = False
self.sdrSource.removeClient(self) self.sdrSource.removeClient(self)
for sub in self.subscriptions: for sub in self.subscriptions:
@ -208,16 +341,15 @@ class DspManager(Output, SdrSourceEventClient):
if state is SdrSourceState.RUNNING: if state is SdrSourceState.RUNNING:
logger.debug("received STATE_RUNNING, attempting DspSource restart") logger.debug("received STATE_RUNNING, attempting DspSource restart")
if self.startOnAvailable: if self.startOnAvailable:
self.dsp.setBuffer(self.sdrSource.getBuffer()) self.chain.setReader(self.sdrSource.getBuffer().getReader())
self.dsp.start()
self.startOnAvailable = False self.startOnAvailable = False
elif state is SdrSourceState.STOPPING: elif state is SdrSourceState.STOPPING:
logger.debug("received STATE_STOPPING, shutting down DspSource") logger.debug("received STATE_STOPPING, shutting down DspSource")
self.dsp.stop() self.stop()
def onFail(self): def onFail(self):
logger.debug("received onFail(), shutting down DspSource") logger.debug("received onFail(), shutting down DspSource")
self.dsp.stop() self.stop()
def onShutdown(self): def onShutdown(self):
self.dsp.stop() self.stop()