openwebrx-clone/owrx/dsp.py

724 lines
28 KiB
Python
Raw Normal View History

from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
2021-10-15 14:41:07 +00:00
from owrx.modes import Modes, DigitalMode
2021-08-23 12:25:28 +00:00
from csdr.chain import Chain
2021-12-13 12:26:47 +00:00
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain, DemodulatorError
2021-09-23 16:43:41 +00:00
from csdr.chain.selector import Selector, SecondarySelector
2021-08-23 12:25:28 +00:00
from csdr.chain.clientaudio import ClientAudioChain
2021-08-27 22:10:46 +00:00
from csdr.chain.fft import FftChain
from csdr.chain.dummy import DummyDemodulator
2021-08-23 12:25:28 +00:00
from pycsdr.modules import Buffer, Writer
from pycsdr.types import Format
2021-09-20 15:24:10 +00:00
from typing import Union, Optional
2021-09-12 22:14:38 +00:00
from io import BytesIO
2021-09-27 16:18:31 +00:00
from abc import ABC, abstractmethod
import threading
import re
2021-09-12 22:14:38 +00:00
import pickle
import logging
logger = logging.getLogger(__name__)
2021-09-27 16:18:31 +00:00
# now that's a name. help, i've reached enterprise level OOP here
class ClientDemodulatorSecondaryDspEventClient(ABC):
@abstractmethod
def onSecondaryDspRateChange(self, rate):
pass
@abstractmethod
def onSecondaryDspBandwidthChange(self, bw):
pass
2021-08-23 12:25:28 +00:00
class ClientDemodulatorChain(Chain):
def __init__(self, demod: BaseDemodulatorChain, sampleRate: int, outputRate: int, hdOutputRate: int, audioCompression: str, nrEnabled: bool, nrThreshold: int, secondaryDspEventReceiver: ClientDemodulatorSecondaryDspEventClient):
2021-08-23 12:25:28 +00:00
self.sampleRate = sampleRate
self.outputRate = outputRate
2021-08-27 16:30:46 +00:00
self.hdOutputRate = hdOutputRate
self.nrEnabled = nrEnabled
self.nrThreshold = nrThreshold
2021-09-27 16:18:31 +00:00
self.secondaryDspEventReceiver = secondaryDspEventReceiver
2021-09-23 16:43:41 +00:00
self.selector = Selector(sampleRate, outputRate)
2021-08-23 12:25:28 +00:00
self.selector.setBandpass(-4000, 4000)
2021-08-27 22:10:46 +00:00
self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT)
2021-08-31 14:54:37 +00:00
self.audioBuffer = None
2021-08-23 12:25:28 +00:00
self.demodulator = demod
2021-08-31 14:54:37 +00:00
self.secondaryDemodulator = None
2021-08-31 20:46:11 +00:00
self.centerFrequency = None
self.frequencyOffset = None
self.wfmDeemphasisTau = 50e-6
2021-08-27 16:30:46 +00:00
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, nrEnabled, nrThreshold)
2021-09-27 15:29:51 +00:00
self.secondaryFftSize = 2048
self.secondaryFftOverlapFactor = 0.3
self.secondaryFftFps = 9
self.secondaryFftCompression = "adpcm"
2021-08-27 22:10:46 +00:00
self.secondaryFftChain = None
self.metaWriter = None
2021-08-31 14:54:37 +00:00
self.secondaryFftWriter = None
self.secondaryWriter = None
self.squelchLevel = -150
2021-09-23 16:43:41 +00:00
self.secondarySelector = None
self.secondaryFrequencyOffset = None
2021-08-23 12:25:28 +00:00
super().__init__([self.selector, self.demodulator, self.clientAudioChain])
2021-08-31 14:54:37 +00:00
def stop(self):
super().stop()
if self.secondaryFftChain is not None:
self.secondaryFftChain.stop()
self.secondaryFftChain = None
if self.secondaryDemodulator is not None:
self.secondaryDemodulator.stop()
self.secondaryDemodulator = None
2021-09-20 15:24:10 +00:00
def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None:
2021-08-27 22:10:46 +00:00
if w1 is self.selector:
super()._connect(w1, w2, self.selectorBuffer)
2021-08-31 14:54:37 +00:00
elif w2 is self.clientAudioChain:
format = w1.getOutputFormat()
if self.audioBuffer is None or self.audioBuffer.getFormat() != format:
self.audioBuffer = Buffer(format)
2021-09-06 13:05:33 +00:00
if self.secondaryDemodulator is not None and self.secondaryDemodulator.getInputFormat() is not Format.COMPLEX_FLOAT:
2021-08-31 14:54:37 +00:00
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
super()._connect(w1, w2, self.audioBuffer)
2021-08-27 22:10:46 +00:00
else:
super()._connect(w1, w2)
2021-08-23 12:25:28 +00:00
def setDemodulator(self, demodulator: BaseDemodulatorChain):
2021-08-31 14:54:37 +00:00
if demodulator is self.demodulator:
return
try:
self.clientAudioChain.setFormat(demodulator.getOutputFormat())
except ValueError:
# this will happen if the new format does not match the current demodulator.
# it's expected and should be mended when swapping out the demodulator in the next step
pass
2021-08-23 12:25:28 +00:00
self.replace(1, demodulator)
if self.demodulator is not None:
self.demodulator.stop()
self.demodulator = demodulator
self.selector.setOutputRate(self._getSelectorOutputRate())
2021-08-23 12:25:28 +00:00
clientRate = self._getClientAudioInputRate()
self.clientAudioChain.setInputRate(clientRate)
self.demodulator.setSampleRate(clientRate)
2021-08-23 12:25:28 +00:00
if isinstance(self.demodulator, DeemphasisTauChain):
self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau)
2021-09-09 20:24:41 +00:00
self._updateDialFrequency()
2021-09-06 13:05:33 +00:00
self._syncSquelch()
2021-08-23 12:25:28 +00:00
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
2021-08-27 16:30:46 +00:00
self.clientAudioChain.setClientRate(outputRate)
if self.metaWriter is not None and isinstance(demodulator, MetaProvider):
demodulator.setMetaWriter(self.metaWriter)
2021-08-23 12:25:28 +00:00
def stopDemodulator(self):
if self.demodulator is None:
return
# we need to get the currrent demodulator out of the chain so that it can be deallocated properly
# so we just replace it with a dummy here
# in order to avoid any client audio chain hassle, the dummy simply imitates the output format of the current
# demodulator
self.replace(1, DummyDemodulator(self.demodulator.getOutputFormat()))
self.demodulator.stop()
self.demodulator = None
2021-09-06 13:05:33 +00:00
def _getSelectorOutputRate(self):
if isinstance(self.demodulator, FixedIfSampleRateChain):
return self.demodulator.getFixedIfSampleRate()
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
2021-09-06 13:05:33 +00:00
if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate():
raise ValueError("secondary and primary demodulator chain audio rates do not match!")
return self.secondaryDemodulator.getFixedAudioRate()
else:
return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
def _getClientAudioInputRate(self):
if isinstance(self.demodulator, FixedAudioRateChain):
return self.demodulator.getFixedAudioRate()
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
return self.secondaryDemodulator.getFixedAudioRate()
else:
return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
2021-09-06 13:05:33 +00:00
2021-09-20 15:24:10 +00:00
def setSecondaryDemodulator(self, demod: Optional[SecondaryDemodulator]):
2021-08-31 14:54:37 +00:00
if demod is self.secondaryDemodulator:
return
if self.secondaryDemodulator is not None:
self.secondaryDemodulator.stop()
self.secondaryDemodulator = demod
2021-09-06 13:05:33 +00:00
rate = self._getSelectorOutputRate()
2021-08-31 14:54:37 +00:00
self.selector.setOutputRate(rate)
clientRate = self._getClientAudioInputRate()
self.clientAudioChain.setInputRate(clientRate)
self.demodulator.setSampleRate(clientRate)
2021-08-31 20:46:11 +00:00
self._updateDialFrequency()
2021-09-06 13:05:33 +00:00
self._syncSquelch()
2021-08-31 14:54:37 +00:00
2021-09-23 16:43:41 +00:00
if isinstance(self.secondaryDemodulator, SecondarySelectorChain):
2021-09-27 16:18:31 +00:00
bandwidth = self.secondaryDemodulator.getBandwidth()
self.secondarySelector = SecondarySelector(rate, bandwidth)
2021-09-23 16:43:41 +00:00
self.secondarySelector.setReader(self.selectorBuffer.getReader())
self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset)
2021-09-27 16:18:31 +00:00
self.secondaryDspEventReceiver.onSecondaryDspBandwidthChange(bandwidth)
2021-09-23 16:43:41 +00:00
else:
self.secondarySelector = None
2021-08-31 14:54:37 +00:00
if self.secondaryDemodulator is not None:
2021-09-23 16:43:41 +00:00
self.secondaryDemodulator.setSampleRate(rate)
if self.secondarySelector is not None:
buffer = Buffer(Format.COMPLEX_FLOAT)
self.secondarySelector.setWriter(buffer)
self.secondaryDemodulator.setReader(buffer.getReader())
elif self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT:
2021-09-06 13:05:33 +00:00
self.secondaryDemodulator.setReader(self.selectorBuffer.getReader())
else:
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
2021-08-31 14:54:37 +00:00
self.secondaryDemodulator.setWriter(self.secondaryWriter)
if self.secondaryDemodulator is None and self.secondaryFftChain is not None:
self.secondaryFftChain.stop()
self.secondaryFftChain = None
if self.secondaryDemodulator is not None and self.secondaryFftChain is None:
2021-09-27 15:29:51 +00:00
self._createSecondaryFftChain()
2021-08-31 14:54:37 +00:00
2021-09-06 13:05:33 +00:00
if self.secondaryFftChain is not None:
self.secondaryFftChain.setSampleRate(rate)
2021-09-27 16:18:31 +00:00
self.secondaryDspEventReceiver.onSecondaryDspRateChange(rate)
2021-09-06 13:05:33 +00:00
2021-09-27 15:29:51 +00:00
def _createSecondaryFftChain(self):
if self.secondaryFftChain is not None:
self.secondaryFftChain.stop()
self.secondaryFftChain = FftChain(self._getSelectorOutputRate(), self.secondaryFftSize, self.secondaryFftOverlapFactor, self.secondaryFftFps, self.secondaryFftCompression)
self.secondaryFftChain.setReader(self.selectorBuffer.getReader())
self.secondaryFftChain.setWriter(self.secondaryFftWriter)
2021-09-06 13:05:33 +00:00
def _syncSquelch(self):
if not self.demodulator.supportsSquelch() or (self.secondaryDemodulator is not None and not self.secondaryDemodulator.supportsSquelch()):
self.selector.setSquelchLevel(-150)
else:
self.selector.setSquelchLevel(self.squelchLevel)
2021-08-23 12:25:28 +00:00
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:
2021-08-31 20:46:11 +00:00
if offset == self.frequencyOffset:
return
self.frequencyOffset = offset
2021-09-23 16:43:41 +00:00
self.selector.setFrequencyOffset(offset)
2021-08-31 20:46:11 +00:00
self._updateDialFrequency()
def setCenterFrequency(self, frequency: int) -> None:
if frequency == self.centerFrequency:
return
self.centerFrequency = frequency
self._updateDialFrequency()
def _updateDialFrequency(self):
if self.centerFrequency is None or self.frequencyOffset is None:
return
dialFrequency = self.centerFrequency + self.frequencyOffset
if isinstance(self.demodulator, DialFrequencyReceiver):
self.demodulator.setDialFrequency(dialFrequency)
2021-08-31 20:46:11 +00:00
if isinstance(self.secondaryDemodulator, DialFrequencyReceiver):
self.secondaryDemodulator.setDialFrequency(dialFrequency)
2021-08-23 12:25:28 +00:00
def setAudioCompression(self, compression: str) -> None:
self.clientAudioChain.setAudioCompression(compression)
def setNrEnabled(self, nrEnabled: bool) -> None:
self.clientAudioChain.setNrEnabled(nrEnabled)
def setNrThreshold(self, nrThreshold: int) -> None:
self.clientAudioChain.setNrThreshold(nrThreshold)
2021-08-23 12:25:28 +00:00
def setSquelchLevel(self, level: float) -> None:
if level == self.squelchLevel:
return
self.squelchLevel = level
2021-09-06 13:05:33 +00:00
self._syncSquelch()
2021-08-23 12:25:28 +00:00
def setOutputRate(self, outputRate) -> None:
if outputRate == self.outputRate:
return
self.outputRate = outputRate
2021-08-27 16:30:46 +00:00
if isinstance(self.demodulator, HdAudio):
return
self._updateDemodulatorOutputRate(outputRate)
2021-08-27 16:30:46 +00:00
def setHdOutputRate(self, outputRate) -> None:
if outputRate == self.hdOutputRate:
return
self.hdOutputRate = outputRate
if not isinstance(self.demodulator, HdAudio):
return
self._updateDemodulatorOutputRate(outputRate)
def _updateDemodulatorOutputRate(self, outputRate):
2021-08-27 15:34:48 +00:00
if not isinstance(self.demodulator, FixedIfSampleRateChain):
2021-08-23 12:25:28 +00:00
self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate)
if self.secondaryDemodulator is not None:
self.secondaryDemodulator.setSampleRate(outputRate)
2021-08-27 15:34:48 +00:00
if not isinstance(self.demodulator, FixedAudioRateChain):
2021-08-23 12:25:28 +00:00
self.clientAudioChain.setClientRate(outputRate)
2021-08-27 22:10:46 +00:00
def setSampleRate(self, sampleRate: int) -> None:
if sampleRate == self.sampleRate:
return
self.sampleRate = sampleRate
self.selector.setInputRate(sampleRate)
2021-08-23 12:25:28 +00:00
def setPowerWriter(self, writer: Writer) -> None:
self.selector.setPowerWriter(writer)
def setMetaWriter(self, writer: Writer) -> None:
if writer is self.metaWriter:
return
self.metaWriter = writer
if isinstance(self.demodulator, MetaProvider):
self.demodulator.setMetaWriter(self.metaWriter)
2021-08-31 14:54:37 +00:00
def setSecondaryFftWriter(self, writer: Writer) -> None:
if writer is self.secondaryFftWriter:
return
self.secondaryFftWriter = writer
2021-08-27 22:10:46 +00:00
2021-08-31 14:54:37 +00:00
if self.secondaryFftChain is not None:
2021-08-27 22:10:46 +00:00
self.secondaryFftChain.setWriter(writer)
2021-08-31 14:54:37 +00:00
def setSecondaryWriter(self, writer: Writer) -> None:
if writer is self.secondaryWriter:
return
self.secondaryWriter = writer
if self.secondaryDemodulator is not None:
self.secondaryDemodulator.setWriter(writer)
def setSlotFilter(self, filter: int) -> None:
if not isinstance(self.demodulator, SlotFilterChain):
2021-09-07 15:37:32 +00:00
return
self.demodulator.setSlotFilter(filter)
2021-08-27 22:10:46 +00:00
def setSecondaryFftSize(self, size: int) -> None:
2021-09-27 15:29:51 +00:00
if size == self.secondaryFftSize:
return
self.secondaryFftSize = size
if not self.secondaryFftChain:
return
self._createSecondaryFftChain()
2021-08-23 12:25:28 +00:00
2021-09-23 16:43:41 +00:00
def setSecondaryFrequencyOffset(self, freq: int) -> None:
if self.secondaryFrequencyOffset == freq:
return
self.secondaryFrequencyOffset = freq
if self.secondarySelector is None:
return
self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset)
2021-09-27 15:29:51 +00:00
def setSecondaryFftCompression(self, compression: str) -> None:
if compression == self.secondaryFftCompression:
return
self.secondaryFftCompression = compression
if not self.secondaryFftChain:
return
self.secondaryFftChain.setCompression(self.secondaryFftCompression)
def setSecondaryFftOverlapFactor(self, overlap: float) -> None:
if overlap == self.secondaryFftOverlapFactor:
return
self.secondaryFftOverlapFactor = overlap
if not self.secondaryFftChain:
return
self.secondaryFftChain.setVOverlapFactor(self.secondaryFftOverlapFactor)
def setSecondaryFftFps(self, fps: int) -> None:
if fps == self.secondaryFftFps:
return
self.secondaryFftFps = fps
if not self.secondaryFftChain:
return
self.secondaryFftChain.setFps(self.secondaryFftFps)
def getSecondaryFftOutputFormat(self) -> Format:
if self.secondaryFftCompression == "adpcm":
return Format.CHAR
return Format.SHORT
def setWfmDeemphasisTau(self, tau: float) -> None:
if tau == self.wfmDeemphasisTau:
return
self.wfmDeemphasisTau = tau
if isinstance(self.demodulator, DeemphasisTauChain):
self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau)
2021-08-23 12:25:28 +00:00
class ModulationValidator(OrValidator):
"""
This validator only allows alphanumeric characters and numbers, but no spaces or special characters
"""
2021-01-24 21:54:58 +00:00
def __init__(self):
super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$")))
2021-09-27 16:18:31 +00:00
class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient):
def __init__(self, handler, sdrSource):
self.handler = handler
self.sdrSource = sdrSource
2020-03-24 21:13:42 +00:00
self.props = PropertyStack()
2021-09-27 15:29:51 +00:00
# current audio mode. should be "audio" or "hd_audio" depending on what demodulatur is in use.
self.audioOutput = None
2020-03-24 21:13:42 +00:00
# local demodulator properties not forwarded to the sdr
# ensure strict validation since these can be set from the client
# and are used to build executable commands
validators = {
"output_rate": "int",
"hd_output_rate": "int",
"squelch_level": "num",
"secondary_mod": ModulationValidator(),
"low_cut": "num",
"high_cut": "num",
"offset_freq": "int",
"mod": ModulationValidator(),
"secondary_offset_freq": "int",
"dmr_filter": "int",
"nr_enabled": "bool",
"nr_threshold": "int",
}
self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators)
self.props.addLayer(0, self.localProps)
2020-03-24 21:13:42 +00:00
# properties that we inherit from the sdr
self.props.addLayer(
1,
self.sdrSource.getProps().filter(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"samp_rate",
"center_freq",
"start_mod",
"start_freq",
"wfm_deemphasis_tau",
2021-05-29 16:50:17 +00:00
"digital_voice_codecserver",
),
)
# defaults for values that may not be set
self.props.addLayer(
2,
PropertyLayer(
output_rate=12000,
hd_output_rate=48000,
digital_voice_codecserver="",
nr_enabled=False,
nr_threshold=0
).readonly()
)
2021-08-23 12:25:28 +00:00
self.chain = ClientDemodulatorChain(
self._getDemodulator("nfm"),
self.props["samp_rate"],
self.props["output_rate"],
2021-08-27 16:30:46 +00:00
self.props["hd_output_rate"],
2021-09-27 16:18:31 +00:00
self.props["audio_compression"],
self.props["nr_enabled"],
self.props["nr_threshold"],
2021-09-27 16:18:31 +00:00
self
2021-08-23 12:25:28 +00:00
)
2021-08-27 14:11:03 +00:00
self.readers = {}
2021-08-26 15:21:52 +00:00
2020-04-26 20:46:30 +00:00
if "start_mod" in self.props:
mode = Modes.findByModulation(self.props["start_mod"])
2021-10-15 14:41:07 +00:00
if mode:
self.setDemodulator(mode.get_modulation())
if isinstance(mode, DigitalMode):
self.setSecondaryDemodulator(mode.modulation)
if mode.bandpass:
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
self.chain.setBandpass(*bpf)
else:
# TODO modes should be mandatory
self.setDemodulator(self.props["start_mod"])
2020-04-26 20:46:30 +00:00
if "start_freq" in self.props and "center_freq" in self.props:
2021-08-23 12:25:28 +00:00
self.chain.setFrequencyOffset(self.props["start_freq"] - self.props["center_freq"])
2020-04-26 20:46:30 +00:00
else:
2021-08-23 12:25:28 +00:00
self.chain.setFrequencyOffset(0)
2020-04-26 20:46:30 +00:00
self.subscriptions = [
2021-08-27 14:11:03 +00:00
self.props.wireProperty("audio_compression", self.setAudioCompression),
2021-09-27 15:29:51 +00:00
self.props.wireProperty("fft_compression", self.chain.setSecondaryFftCompression),
self.props.wireProperty("fft_voverlap_factor", self.chain.setSecondaryFftOverlapFactor),
self.props.wireProperty("fft_fps", self.chain.setSecondaryFftFps),
2021-09-27 16:18:31 +00:00
self.props.wireProperty("digimodes_fft_size", self.setSecondaryFftSize),
2021-08-23 12:25:28 +00:00
self.props.wireProperty("samp_rate", self.chain.setSampleRate),
self.props.wireProperty("output_rate", self.chain.setOutputRate),
2021-08-27 16:30:46 +00:00
self.props.wireProperty("hd_output_rate", self.chain.setHdOutputRate),
2021-08-23 12:25:28 +00:00
self.props.wireProperty("offset_freq", self.chain.setFrequencyOffset),
2021-08-31 20:46:11 +00:00
self.props.wireProperty("center_freq", self.chain.setCenterFrequency),
2021-08-23 12:25:28 +00:00
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),
self.props.wireProperty("dmr_filter", self.chain.setSlotFilter),
self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau),
2021-08-27 22:10:46 +00:00
self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator),
2021-09-23 16:43:41 +00:00
self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset),
self.props.wireProperty("nr_enabled", self.chain.setNrEnabled),
self.props.wireProperty("nr_threshold", self.chain.setNrThreshold),
]
2021-09-27 15:29:51 +00:00
# wire power level output
buffer = Buffer(Format.FLOAT)
self.chain.setPowerWriter(buffer)
self.wireOutput("smeter", buffer)
# wire meta output
buffer = Buffer(Format.CHAR)
self.chain.setMetaWriter(buffer)
self.wireOutput("meta", buffer)
# wire secondary FFT
buffer = Buffer(self.chain.getSecondaryFftOutputFormat())
self.chain.setSecondaryFftWriter(buffer)
self.wireOutput("secondary_fft", buffer)
# wire secondary demodulator
buffer = Buffer(Format.CHAR)
self.chain.setSecondaryWriter(buffer)
self.wireOutput("secondary_demod", buffer)
2020-04-30 20:54:44 +00:00
self.startOnAvailable = False
self.sdrSource.addClient(self)
2021-09-27 16:18:31 +00:00
def setSecondaryFftSize(self, size):
self.chain.setSecondaryFftSize(size)
self.handler.write_secondary_dsp_config({"secondary_fft_size": size})
2021-09-23 16:43:41 +00:00
def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]) -> Optional[BaseDemodulatorChain]:
2021-08-23 12:25:28 +00:00
if isinstance(demod, BaseDemodulatorChain):
return demod
# TODO: move this to Modes
if demod == "nfm":
from csdr.chain.analog import NFm
2021-09-07 12:45:52 +00:00
return NFm(self.props["output_rate"])
2021-08-23 12:25:28 +00:00
elif demod == "wfm":
from csdr.chain.analog import WFm
2021-09-07 12:45:52 +00:00
return WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"])
2021-08-23 12:25:28 +00:00
elif demod == "am":
from csdr.chain.analog import Am
2021-09-07 12:45:52 +00:00
return Am()
2021-08-23 12:25:28 +00:00
elif demod in ["usb", "lsb", "cw"]:
from csdr.chain.analog import Ssb
2021-09-07 12:45:52 +00:00
return Ssb()
2021-08-23 12:25:28 +00:00
elif demod == "dmr":
from csdr.chain.digiham import Dmr
2021-09-07 12:45:52 +00:00
return Dmr(self.props["digital_voice_codecserver"])
2021-08-23 12:25:28 +00:00
elif demod == "dstar":
from csdr.chain.digiham import Dstar
2021-09-07 12:45:52 +00:00
return Dstar(self.props["digital_voice_codecserver"])
2021-08-23 12:25:28 +00:00
elif demod == "ysf":
from csdr.chain.digiham import Ysf
2021-09-07 12:45:52 +00:00
return Ysf(self.props["digital_voice_codecserver"])
2021-08-23 12:25:28 +00:00
elif demod == "nxdn":
from csdr.chain.digiham import Nxdn
2021-09-07 12:45:52 +00:00
return Nxdn(self.props["digital_voice_codecserver"])
elif demod == "m17":
from csdr.chain.m17 import M17
2021-09-20 13:32:26 +00:00
return M17()
2021-09-07 15:31:32 +00:00
elif demod == "drm":
from csdr.chain.drm import Drm
2021-09-07 15:31:32 +00:00
return Drm()
elif demod == "freedv":
from csdr.chain.freedv import FreeDV
return FreeDV()
2021-08-23 12:25:28 +00:00
def setDemodulator(self, mod):
self.chain.stopDemodulator()
2021-12-13 12:26:47 +00:00
try:
demodulator = self._getDemodulator(mod)
if demodulator is None:
raise ValueError("unsupported demodulator: {}".format(mod))
self.chain.setDemodulator(demodulator)
output = "hd_audio" if isinstance(demodulator, HdAudio) else "audio"
if output != self.audioOutput:
self.audioOutput = output
# re-wire the audio to the correct client API
buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer)
self.wireOutput(self.audioOutput, buffer)
except DemodulatorError as de:
self.handler.write_demodulator_error(str(de))
2021-08-27 16:30:46 +00:00
2021-09-23 16:43:41 +00:00
def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]:
2021-08-31 14:54:37 +00:00
if isinstance(mod, SecondaryDemodulator):
return mod
2021-08-31 15:01:52 +00:00
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
2021-09-20 14:53:00 +00:00
from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.wsjt import WsjtParser
2021-08-31 14:54:37 +00:00
return AudioChopperDemodulator(mod, WsjtParser())
2021-09-06 20:50:57 +00:00
elif mod == "js8":
2021-09-20 14:53:00 +00:00
from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.js8 import Js8Parser
2021-09-06 20:50:57 +00:00
return AudioChopperDemodulator(mod, Js8Parser())
2021-09-06 13:05:33 +00:00
elif mod == "packet":
2021-09-20 14:53:00 +00:00
from csdr.chain.digimodes import PacketDemodulator
2021-09-06 13:05:33 +00:00
return PacketDemodulator()
2021-09-06 18:00:14 +00:00
elif mod == "pocsag":
2021-09-20 14:53:00 +00:00
from csdr.chain.digimodes import PocsagDemodulator
2021-09-06 18:00:14 +00:00
return PocsagDemodulator()
2021-09-23 16:43:41 +00:00
elif mod == "bpsk31":
from csdr.chain.digimodes import PskDemodulator
return PskDemodulator(31.25)
elif mod == "bpsk63":
from csdr.chain.digimodes import PskDemodulator
return PskDemodulator(62.5)
2021-08-31 14:54:37 +00:00
2021-08-27 22:10:46 +00:00
def setSecondaryDemodulator(self, mod):
2021-08-31 14:54:37 +00:00
demodulator = self._getSecondaryDemodulator(mod)
if not demodulator:
self.chain.setSecondaryDemodulator(None)
2021-08-27 22:10:46 +00:00
else:
2021-08-31 14:54:37 +00:00
self.chain.setSecondaryDemodulator(demodulator)
2021-08-27 22:10:46 +00:00
2021-08-27 14:11:03 +00:00
def setAudioCompression(self, comp):
try:
self.chain.setAudioCompression(comp)
except ValueError:
# wrong output format... need to re-wire
buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer)
2021-09-27 16:18:31 +00:00
self.wireOutput(self.audioOutput, buffer)
2021-08-27 14:11:03 +00:00
def start(self):
if self.sdrSource.isAvailable():
2021-08-23 12:25:28 +00:00
self.chain.setReader(self.sdrSource.getBuffer().getReader())
2020-04-30 20:54:44 +00:00
else:
self.startOnAvailable = True
2021-08-27 16:30:46 +00:00
def unwireOutput(self, t: str):
if t in self.readers:
self.readers[t].stop()
del self.readers[t]
2021-08-26 15:21:52 +00:00
def wireOutput(self, t: str, buffer: Buffer):
logger.debug("wiring new output of type %s", t)
writers = {
"audio": self.handler.write_dsp_data,
2020-08-08 19:29:25 +00:00
"hd_audio": self.handler.write_hd_audio,
"smeter": self.handler.write_s_meter_level,
"secondary_fft": self.handler.write_secondary_fft,
2021-09-12 22:14:38 +00:00
"secondary_demod": self._unpickle(self.handler.write_secondary_demod),
"meta": self._unpickle(self.handler.write_metadata),
}
write = writers[t]
2021-08-27 16:30:46 +00:00
self.unwireOutput(t)
2021-08-27 14:11:03 +00:00
2021-08-26 15:21:52 +00:00
reader = buffer.getReader()
2021-08-27 14:11:03 +00:00
self.readers[t] = reader
threading.Thread(target=self.chain.pump(reader.read, write), name="dsp_pump_{}".format(t)).start()
2021-09-12 22:14:38 +00:00
def _unpickle(self, callback):
def unpickler(data):
2021-09-23 16:43:41 +00:00
b = data.tobytes()
io = BytesIO(b)
2021-09-12 22:14:38 +00:00
try:
while True:
callback(pickle.load(io))
except EOFError:
pass
2021-09-23 16:43:41 +00:00
# TODO: this is not ideal. is there a way to know beforehand if the data will be pickled?
except pickle.UnpicklingError:
callback(b.decode("ascii"))
2021-09-12 22:14:38 +00:00
return unpickler
def stop(self):
if self.chain:
self.chain.stop()
self.chain = None
2021-08-27 14:11:03 +00:00
for reader in self.readers.values():
reader.stop()
self.readers = {}
2021-08-23 12:25:28 +00:00
2020-04-30 20:54:44 +00:00
self.startOnAvailable = False
self.sdrSource.removeClient(self)
for sub in self.subscriptions:
sub.cancel()
self.subscriptions = []
def setProperties(self, props):
for k, v in props.items():
self.setProperty(k, v)
def setProperty(self, prop, value):
self.localProps[prop] = value
2021-02-20 21:54:07 +00:00
def getClientClass(self) -> SdrClientClass:
return SdrClientClass.USER
2021-02-20 21:54:07 +00:00
def onStateChange(self, state: SdrSourceState):
if state is SdrSourceState.RUNNING:
logger.debug("received STATE_RUNNING, attempting DspSource restart")
2020-04-30 20:54:44 +00:00
if self.startOnAvailable:
2021-08-23 12:25:28 +00:00
self.chain.setReader(self.sdrSource.getBuffer().getReader())
2020-04-30 20:54:44 +00:00
self.startOnAvailable = False
2021-02-20 21:54:07 +00:00
elif state is SdrSourceState.STOPPING:
logger.debug("received STATE_STOPPING, shutting down DspSource")
2021-08-23 12:25:28 +00:00
self.stop()
def onFail(self):
logger.debug("received onFail(), shutting down DspSource")
2021-08-23 12:25:28 +00:00
self.stop()
def onShutdown(self):
2021-08-23 12:25:28 +00:00
self.stop()
2021-09-27 16:18:31 +00:00
def onSecondaryDspBandwidthChange(self, bw):
self.handler.write_secondary_dsp_config({"secondary_bw": bw})
def onSecondaryDspRateChange(self, rate):
self.handler.write_secondary_dsp_config({"if_samp_rate": rate})