restore PSK decoding

This commit is contained in:
Jakob Ketterl 2021-09-23 18:43:41 +02:00
parent 3fa3aac766
commit cbcba5807f
6 changed files with 145 additions and 51 deletions

View File

@ -1,21 +1,8 @@
from csdr.chain import Chain from csdr.chain import Chain
from abc import ABC, abstractmethod from abc import ABC, ABCMeta, abstractmethod
from pycsdr.modules import Writer from pycsdr.modules import Writer
class BaseDemodulatorChain(Chain):
def supportsSquelch(self) -> bool:
return True
def setSampleRate(self, sampleRate: int) -> None:
pass
class SecondaryDemodulator(Chain):
def supportsSquelch(self) -> bool:
return True
class FixedAudioRateChain(ABC): class FixedAudioRateChain(ABC):
@abstractmethod @abstractmethod
def getFixedAudioRate(self) -> int: def getFixedAudioRate(self) -> int:
@ -49,3 +36,28 @@ class SlotFilterChain(ABC):
@abstractmethod @abstractmethod
def setSlotFilter(self, filter: int) -> None: def setSlotFilter(self, filter: int) -> None:
pass pass
class SecondarySelectorChain(ABC):
def getBandwidth(self) -> float:
pass
class BaseDemodulatorChain(Chain):
def supportsSquelch(self) -> bool:
return True
def setSampleRate(self, sampleRate: int) -> None:
pass
class SecondaryDemodulator(Chain):
def supportsSquelch(self) -> bool:
return True
def setSampleRate(self, sampleRate: int) -> None:
pass
class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta):
pass

View File

@ -1,15 +1,15 @@
from csdr.chain.demodulator import SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain
from owrx.audio.chopper import AudioChopper from owrx.audio.chopper import AudioChopper
from owrx.aprs.kiss import KissDeframer from owrx.aprs.kiss import KissDeframer
from owrx.aprs import Ax25Parser, AprsParser from owrx.aprs import Ax25Parser, AprsParser
from pycsdr.modules import Convert, FmDemod from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder
from pycsdr.types import Format from pycsdr.types import Format
from owrx.aprs.module import DirewolfModule from owrx.aprs.module import DirewolfModule
from digiham.modules import FskDemodulator, PocsagDecoder from digiham.modules import FskDemodulator, PocsagDecoder
from owrx.pocsag import PocsagParser from owrx.pocsag import PocsagParser
class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
# TODO parser typing # TODO parser typing
def __init__(self, mode: str, parser): def __init__(self, mode: str, parser):
self.chopper = AudioChopper(mode, parser) self.chopper = AudioChopper(mode, parser)
@ -23,7 +23,7 @@ class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFre
self.chopper.setDialFrequency(frequency) self.chopper.setDialFrequency(frequency)
class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver): class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False): def __init__(self, service: bool = False):
self.parser = AprsParser() self.parser = AprsParser()
workers = [ workers = [
@ -46,7 +46,7 @@ class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequency
self.parser.setDialFrequency(frequency) self.parser.setDialFrequency(frequency)
class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain): class PocsagDemodulator(ServiceDemodulator):
def __init__(self): def __init__(self):
workers = [ workers = [
FmDemod(), FmDemod(),
@ -61,3 +61,28 @@ class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain):
def getFixedAudioRate(self) -> int: def getFixedAudioRate(self) -> int:
return 48000 return 48000
class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
def __init__(self, baudRate: float):
self.baudRate = baudRate
# this is an assumption, we will adjust in setSampleRate
self.sampleRate = 12000
secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
workers = [
Agc(Format.COMPLEX_FLOAT),
TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True),
DBPskDecoder(),
VaricodeDecoder(),
]
super().__init__(workers)
def getBandwidth(self):
return self.baudRate
def setSampleRate(self, sampleRate: int) -> None:
if sampleRate == self.sampleRate:
return
self.sampleRate = sampleRate
secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
self.replace(1, TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True))

View File

@ -62,10 +62,12 @@ class Decimator(Chain):
class Selector(Chain): class Selector(Chain):
def __init__(self, inputRate: int, outputRate: int, shiftRate: float, withSquelch: bool = True): def __init__(self, inputRate: int, outputRate: int, withSquelch: bool = True):
self.inputRate = inputRate
self.outputRate = outputRate self.outputRate = outputRate
self.frequencyOffset = 0
self.shift = Shift(shiftRate) self.shift = Shift(0.0)
self.decimation = Decimator(inputRate, outputRate) self.decimation = Decimator(inputRate, outputRate)
@ -88,8 +90,15 @@ class Selector(Chain):
bp_transition = 320.0 / self.outputRate bp_transition = 320.0 / self.outputRate
return Bandpass(transition=bp_transition, use_fft=True) return Bandpass(transition=bp_transition, use_fft=True)
def setShiftRate(self, rate: float) -> None: def setFrequencyOffset(self, offset: int) -> None:
self.shift.setRate(rate) if offset == self.frequencyOffset:
return
self.frequencyOffset = offset
self._updateShift()
def _updateShift(self):
shift = -self.frequencyOffset / self.inputRate
self.shift.setRate(shift)
def _convertToLinear(self, db: float) -> float: def _convertToLinear(self, db: float) -> float:
return float(math.pow(10, db / 10)) return float(math.pow(10, db / 10))
@ -125,4 +134,27 @@ class Selector(Chain):
self.replace(2, self.bandpass) self.replace(2, self.bandpass)
def setInputRate(self, inputRate: int) -> None: def setInputRate(self, inputRate: int) -> None:
if inputRate == self.inputRate:
return
self.inputRate = inputRate
self.decimation.setInputRate(inputRate) self.decimation.setInputRate(inputRate)
self._updateShift()
class SecondarySelector(Chain):
def __init__(self, sampleRate: int, bandwidth: float):
self.sampleRate = sampleRate
self.frequencyOffset = 0
self.shift = Shift(0.0)
cutoffRate = bandwidth / sampleRate
self.bandpass = Bandpass(-cutoffRate, cutoffRate, cutoffRate, use_fft=True)
workers = [self.shift, self.bandpass]
super().__init__(workers)
def setFrequencyOffset(self, offset: int) -> None:
if offset == self.frequencyOffset:
return
self.frequencyOffset = offset
if self.frequencyOffset is None:
return
self.shift.setRate(-offset / self.sampleRate)

View File

@ -3,8 +3,8 @@ from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
from owrx.modes import Modes from owrx.modes import Modes
from csdr.chain import Chain from csdr.chain import Chain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain
from csdr.chain.selector import Selector from csdr.chain.selector import Selector, SecondarySelector
from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.clientaudio import ClientAudioChain
from csdr.chain.fft import FftChain from csdr.chain.fft import FftChain
from pycsdr.modules import Buffer, Writer from pycsdr.modules import Buffer, Writer
@ -25,7 +25,7 @@ class ClientDemodulatorChain(Chain):
self.sampleRate = sampleRate self.sampleRate = sampleRate
self.outputRate = outputRate self.outputRate = outputRate
self.hdOutputRate = hdOutputRate self.hdOutputRate = hdOutputRate
self.selector = Selector(sampleRate, outputRate, 0.0) self.selector = Selector(sampleRate, outputRate)
self.selector.setBandpass(-4000, 4000) self.selector.setBandpass(-4000, 4000)
self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT) self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT)
self.audioBuffer = None self.audioBuffer = None
@ -41,6 +41,8 @@ class ClientDemodulatorChain(Chain):
self.secondaryFftWriter = None self.secondaryFftWriter = None
self.secondaryWriter = None self.secondaryWriter = None
self.squelchLevel = -150 self.squelchLevel = -150
self.secondarySelector = None
self.secondaryFrequencyOffset = None
super().__init__([self.selector, self.demodulator, self.clientAudioChain]) super().__init__([self.selector, self.demodulator, self.clientAudioChain])
def stop(self): def stop(self):
@ -131,8 +133,20 @@ class ClientDemodulatorChain(Chain):
self._updateDialFrequency() self._updateDialFrequency()
self._syncSquelch() self._syncSquelch()
if isinstance(self.secondaryDemodulator, SecondarySelectorChain):
self.secondarySelector = SecondarySelector(rate, self.secondaryDemodulator.getBandwidth())
self.secondarySelector.setReader(self.selectorBuffer.getReader())
self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset)
else:
self.secondarySelector = None
if self.secondaryDemodulator is not None: if self.secondaryDemodulator is not None:
if self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT: 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:
self.secondaryDemodulator.setReader(self.selectorBuffer.getReader()) self.secondaryDemodulator.setReader(self.selectorBuffer.getReader())
else: else:
self.secondaryDemodulator.setReader(self.audioBuffer.getReader()) self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
@ -170,10 +184,7 @@ class ClientDemodulatorChain(Chain):
if offset == self.frequencyOffset: if offset == self.frequencyOffset:
return return
self.frequencyOffset = offset self.frequencyOffset = offset
self.selector.setFrequencyOffset(offset)
shift = -offset / self.sampleRate
self.selector.setShiftRate(shift)
self._updateDialFrequency() self._updateDialFrequency()
def setCenterFrequency(self, frequency: int) -> None: def setCenterFrequency(self, frequency: int) -> None:
@ -211,6 +222,8 @@ class ClientDemodulatorChain(Chain):
if not isinstance(self.demodulator, FixedIfSampleRateChain): if not isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(outputRate) self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate) self.demodulator.setSampleRate(outputRate)
if self.secondaryDemodulator is not None:
self.secondaryDemodulator.setSampleRate(outputRate)
if not isinstance(self.demodulator, FixedAudioRateChain): if not isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setClientRate(outputRate) self.clientAudioChain.setClientRate(outputRate)
@ -268,6 +281,15 @@ class ClientDemodulatorChain(Chain):
# TODO # TODO
pass pass
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)
class ModulationValidator(OrValidator): class ModulationValidator(OrValidator):
""" """
@ -397,8 +419,6 @@ class DspManager(SdrSourceEventClient):
self.props.wireProperty("dmr_filter", self.chain.setSlotFilter), self.props.wireProperty("dmr_filter", self.chain.setSlotFilter),
# TODO # TODO
# self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau), # self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau),
# TODO
# self.props.wireProperty("digital_voice_codecserver", self.dsp.set_codecserver),
] ]
# TODO # TODO
@ -414,8 +434,7 @@ class DspManager(SdrSourceEventClient):
self.subscriptions += [ self.subscriptions += [
self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator), self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator),
self.props.wireProperty("digimodes_fft_size", self.chain.setSecondaryFftSize), self.props.wireProperty("digimodes_fft_size", self.chain.setSecondaryFftSize),
# TODO self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset),
# self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
] ]
self.startOnAvailable = False self.startOnAvailable = False
@ -424,7 +443,7 @@ class DspManager(SdrSourceEventClient):
super().__init__() super().__init__()
def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]): def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]) -> Optional[BaseDemodulatorChain]:
if isinstance(demod, BaseDemodulatorChain): if isinstance(demod, BaseDemodulatorChain):
return demod return demod
# TODO: move this to Modes # TODO: move this to Modes
@ -486,7 +505,7 @@ class DspManager(SdrSourceEventClient):
} }
) )
def _getSecondaryDemodulator(self, mod): def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]:
if isinstance(mod, SecondaryDemodulator): if isinstance(mod, SecondaryDemodulator):
return mod return mod
# TODO add remaining modes # TODO add remaining modes
@ -504,7 +523,12 @@ class DspManager(SdrSourceEventClient):
elif mod == "pocsag": elif mod == "pocsag":
from csdr.chain.digimodes import PocsagDemodulator from csdr.chain.digimodes import PocsagDemodulator
return PocsagDemodulator() return PocsagDemodulator()
return None 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)
def setSecondaryDemodulator(self, mod): def setSecondaryDemodulator(self, mod):
demodulator = self._getSecondaryDemodulator(mod) demodulator = self._getSecondaryDemodulator(mod)
@ -556,12 +580,16 @@ class DspManager(SdrSourceEventClient):
def _unpickle(self, callback): def _unpickle(self, callback):
def unpickler(data): def unpickler(data):
io = BytesIO(data.tobytes()) b = data.tobytes()
io = BytesIO(b)
try: try:
while True: while True:
callback(pickle.load(io)) callback(pickle.load(io))
except EOFError: except EOFError:
pass pass
# 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"))
return unpickler return unpickler

View File

@ -8,8 +8,8 @@ from owrx.property import PropertyLayer, PropertyDeleted
from owrx.service.schedule import ServiceScheduler from owrx.service.schedule import ServiceScheduler
from owrx.service.chain import ServiceDemodulatorChain from owrx.service.chain import ServiceDemodulatorChain
from owrx.modes import Modes, DigitalMode from owrx.modes import Modes, DigitalMode
from typing import Union from typing import Union, Optional
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, DialFrequencyReceiver from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator, DialFrequencyReceiver
from pycsdr.modules import Buffer from pycsdr.modules import Buffer
import logging import logging
@ -250,12 +250,11 @@ class ServiceHandler(SdrSourceEventClient):
secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation) secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation)
center_freq = source.getProps()["center_freq"] center_freq = source.getProps()["center_freq"]
sampleRate = source.getProps()["samp_rate"] sampleRate = source.getProps()["samp_rate"]
shift = (center_freq - frequency) / sampleRate
bandpass = modeObject.get_bandpass() bandpass = modeObject.get_bandpass()
if isinstance(secondaryDemod, DialFrequencyReceiver): if isinstance(secondaryDemod, DialFrequencyReceiver):
secondaryDemod.setDialFrequency(frequency) secondaryDemod.setDialFrequency(frequency)
chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, shift) chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, frequency - center_freq)
chain.setBandPass(bandpass.low_cut, bandpass.high_cut) chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
chain.setReader(source.getBuffer().getReader()) chain.setReader(source.getBuffer().getReader())
@ -277,8 +276,8 @@ class ServiceHandler(SdrSourceEventClient):
return Ssb() return Ssb()
# TODO move this elsewhere # TODO move this elsewhere
def _getSecondaryDemodulator(self, mod): def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]:
if isinstance(mod, SecondaryDemodulator): if isinstance(mod, ServiceDemodulatorChain):
return mod return mod
# TODO add remaining modes # TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:

View File

@ -1,15 +1,13 @@
from csdr.chain import Chain from csdr.chain import Chain
from csdr.chain.selector import Selector from csdr.chain.selector import Selector
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, FixedAudioRateChain from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator
from pycsdr.types import Format from pycsdr.types import Format
class ServiceDemodulatorChain(Chain): class ServiceDemodulatorChain(Chain):
def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: SecondaryDemodulator, sampleRate: int, shiftRate: float): def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: ServiceDemodulator, sampleRate: int, frequencyOffset: int):
# TODO magic number... check if this edge case even exsists and change the api if possible self.selector = Selector(sampleRate, secondaryDemod.getFixedAudioRate(), withSquelch=False)
rate = secondaryDemod.getFixedAudioRate() if isinstance(secondaryDemod, FixedAudioRateChain) else 1200 self.selector.setFrequencyOffset(frequencyOffset)
self.selector = Selector(sampleRate, rate, shiftRate, withSquelch=False)
workers = [self.selector] workers = [self.selector]