openwebrx-clone/owrx/dsp.py

474 lines
17 KiB
Python
Raw Normal View History

from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser
2020-04-12 11:10:23 +00:00
from owrx.js8 import Js8Parser
from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
2020-04-26 20:46:30 +00:00
from owrx.modes import Modes
2021-04-09 16:16:25 +00:00
from csdr.output import Output
2021-08-23 12:25:28 +00:00
from csdr.chain import Chain
2021-08-27 16:30:46 +00:00
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio
2021-08-23 12:25:28 +00:00
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 DigihamChain, Dmr, Dstar, Nxdn, Ysf
2021-08-27 22:10:46 +00:00
from csdr.chain.fft import FftChain
2021-08-23 12:25:28 +00:00
from pycsdr.modules import Buffer, Writer
from pycsdr.types import Format
from typing import Union
import threading
import re
import logging
logger = logging.getLogger(__name__)
2021-08-23 12:25:28 +00:00
class ClientDemodulatorChain(Chain):
2021-08-27 16:30:46 +00:00
def __init__(self, demod: BaseDemodulatorChain, sampleRate: int, outputRate: int, hdOutputRate: int, audioCompression: str):
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
2021-08-23 12:25:28 +00:00
self.selector = Selector(sampleRate, outputRate, 0.0)
self.selector.setBandpass(-4000, 4000)
2021-08-27 22:10:46 +00:00
self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT)
2021-08-23 12:25:28 +00:00
self.demodulator = demod
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)
2021-08-27 22:10:46 +00:00
self.secondaryFftChain = None
self.metaWriter = None
self.squelchLevel = -150
2021-08-23 12:25:28 +00:00
super().__init__([self.selector, self.demodulator, self.clientAudioChain])
2021-08-27 22:10:46 +00:00
def _connect(self, w1, w2, buffer: Union[Buffer, None] = None) -> None:
if w1 is self.selector:
super()._connect(w1, w2, self.selectorBuffer)
else:
super()._connect(w1, w2)
2021-08-23 12:25:28 +00:00
def setDemodulator(self, demodulator: BaseDemodulatorChain):
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
2021-08-27 16:30:46 +00:00
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
2021-08-27 15:34:48 +00:00
if isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(self.demodulator.getFixedIfSampleRate())
2021-08-23 12:25:28 +00:00
else:
2021-08-27 16:30:46 +00:00
self.selector.setOutputRate(outputRate)
2021-08-23 12:25:28 +00:00
2021-08-27 15:34:48 +00:00
if isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.demodulator.getFixedAudioRate())
2021-08-23 12:25:28 +00:00
else:
2021-08-27 16:30:46 +00:00
self.clientAudioChain.setInputRate(outputRate)
2021-08-23 12:25:28 +00:00
if not demodulator.supportsSquelch():
self.selector.setSquelchLevel(-150)
else:
self.selector.setSquelchLevel(self.squelchLevel)
2021-08-23 12:25:28 +00:00
2021-08-27 16:30:46 +00:00
self.clientAudioChain.setClientRate(outputRate)
if self.metaWriter is not None and isinstance(demodulator, DigihamChain):
demodulator.setMetaWriter(self.metaWriter)
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:
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 level == self.squelchLevel:
return
self.squelchLevel = level
2021-08-23 12:25:28 +00:00
if not self.demodulator.supportsSquelch():
return
self.selector.setSquelchLevel(level)
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
if not isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(outputRate)
if not isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setClientRate(outputRate)
def setHdOutputRate(self, outputRate) -> None:
if outputRate == self.hdOutputRate:
return
self.hdOutputRate = outputRate
if not isinstance(self.demodulator, HdAudio):
return
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)
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)
# TODO update secondary FFT
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, DigihamChain):
self.demodulator.setMetaWriter(self.metaWriter)
2021-08-27 22:10:46 +00:00
def setSecondaryFftWriter(self, writer: Union[Writer, None]) -> None:
if writer is None:
if self.secondaryFftChain is not None:
self.secondaryFftChain.stop()
self.secondaryFftChain = None
else:
if self.secondaryFftChain is None:
# TODO eliminate constants
self.secondaryFftChain = FftChain(self.outputRate, 2048, 0.3, 9, "adpcm")
self.secondaryFftChain.setReader(self.selectorBuffer.getReader())
self.secondaryFftChain.setWriter(writer)
def setSecondaryFftSize(self, size: int) -> None:
# TODO
pass
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-04-09 16:16:25 +00:00
class DspManager(Output, SdrSourceEventClient):
def __init__(self, handler, sdrSource):
self.handler = handler
self.sdrSource = sdrSource
self.parsers = {
"meta": MetaParser(self.handler),
"wsjt_demod": WsjtParser(self.handler),
"packet_demod": AprsParser(self.handler),
"pocsag_demod": PocsagParser(self.handler),
2020-04-12 11:10:23 +00:00
"js8_demod": Js8Parser(self.handler),
}
2020-03-24 21:13:42 +00:00
self.props = PropertyStack()
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",
}
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",
),
)
2021-08-23 12:25:28 +00:00
# TODO wait for the rate to come from the client
if "output_rate" not in self.props:
self.props["output_rate"] = 12000
2021-08-27 16:30:46 +00:00
if "hd_output_rate" not in self.props:
self.props["hd_output_rate"] = 48000
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-08-23 12:25:28 +00:00
self.props["audio_compression"]
)
2021-08-27 14:11:03 +00:00
self.readers = {}
2021-08-26 15:21:52 +00:00
2021-08-23 12:25:28 +00:00
# wire audio output
buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer)
2021-08-27 16:30:46 +00:00
# TODO check for hd audio
2021-08-26 15:21:52 +00:00
self.wireOutput("audio", buffer)
2021-08-23 12:25:28 +00:00
# wire power level output
buffer = Buffer(Format.FLOAT)
self.chain.setPowerWriter(buffer)
2021-08-26 15:21:52 +00:00
self.wireOutput("smeter", buffer)
# wire meta output
buffer = Buffer(Format.CHAR)
self.chain.setMetaWriter(buffer)
2021-08-26 15:21:52 +00:00
self.wireOutput("meta", buffer)
2020-12-30 16:18:46 +00:00
def set_dial_freq(changes):
if (
"center_freq" not in self.props
or self.props["center_freq"] is None
or "offset_freq" not in self.props
or self.props["offset_freq"] is None
):
2020-10-10 22:15:09 +00:00
return
2020-03-24 21:13:42 +00:00
freq = self.props["center_freq"] + self.props["offset_freq"]
for parser in self.parsers.values():
parser.setDialFrequency(freq)
2020-04-26 20:46:30 +00:00
if "start_mod" in self.props:
2021-08-23 12:25:28 +00:00
self.setDemodulator(self.props["start_mod"])
2020-04-26 20:46:30 +00:00
mode = Modes.findByModulation(self.props["start_mod"])
if mode and mode.bandpass:
2021-08-23 12:25:28 +00:00
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
self.chain.setBandpass(*bpf)
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-08-23 12:25:28 +00:00
# probably unused:
# self.props.wireProperty("fft_compression", self.dsp.set_fft_compression),
2021-08-27 22:10:46 +00:00
self.props.wireProperty("digimodes_fft_size", self.chain.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),
# 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),
2020-03-24 21:16:11 +00:00
self.props.filter("center_freq", "offset_freq").wire(set_dial_freq),
]
2021-08-23 12:25:28 +00:00
# TODO
# sp.set_temporary_directory(CoreConfig().get_temporary_directory())
def set_secondary_mod(mod):
if mod == False:
mod = None
self.dsp.set_secondary_demodulator(mod)
2021-08-27 22:10:46 +00:00
#if mod is not None:
#send_secondary_config()
self.subscriptions += [
2021-08-27 22:10:46 +00:00
self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator),
self.props.wireProperty("digimodes_fft_size", self.chain.setSecondaryFftSize),
2021-08-23 12:25:28 +00:00
# TODO
# self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
]
2020-04-30 20:54:44 +00:00
self.startOnAvailable = False
self.sdrSource.addClient(self)
super().__init__()
2021-08-23 12:25:28 +00:00
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":
2021-08-27 16:30:46 +00:00
demodChain = WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"])
2021-08-23 12:25:28 +00:00
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)
2021-08-27 16:30:46 +00:00
# re-wire the audio to the correct client API
buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer)
if isinstance(demodulator, HdAudio):
self.wireOutput("hd_audio", buffer)
else:
self.wireOutput("audio", buffer)
2021-08-27 22:10:46 +00:00
def sendSecondaryConfig(self):
self.handler.write_secondary_dsp_config(
{
"secondary_fft_size": self.props["digimodes_fft_size"],
"if_samp_rate": self.props["output_rate"],
# TODO
"secondary_bw": 31.25
}
)
def setSecondaryDemodulator(self, mod):
if not mod:
self.chain.setSecondaryFftWriter(None)
else:
buffer = Buffer(Format.CHAR)
self.chain.setSecondaryFftWriter(buffer)
self.wireOutput("secondary_fft", buffer)
self.sendSecondaryConfig()
#self.chain.setSecondaryDemodulator(mod)
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-08-27 16:30:46 +00:00
# TODO check if this is hd audio
2021-08-27 14:11:03 +00:00
self.wireOutput("audio", buffer)
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,
"secondary_demod": self.handler.write_secondary_demod,
}
for demod, parser in self.parsers.items():
writers[demod] = parser.parse
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
2021-08-26 15:21:52 +00:00
threading.Thread(target=self.pump(reader.read, write), name="dsp_pump_{}".format(t)).start()
def stop(self):
2021-08-23 12:25:28 +00:00
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()