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 abc import ABC, abstractmethod
from abc import ABC, ABCMeta, abstractmethod
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):
@abstractmethod
def getFixedAudioRate(self) -> int:
@ -49,3 +36,28 @@ class SlotFilterChain(ABC):
@abstractmethod
def setSlotFilter(self, filter: int) -> None:
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.aprs.kiss import KissDeframer
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 owrx.aprs.module import DirewolfModule
from digiham.modules import FskDemodulator, PocsagDecoder
from owrx.pocsag import PocsagParser
class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver):
class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
# TODO parser typing
def __init__(self, mode: str, parser):
self.chopper = AudioChopper(mode, parser)
@ -23,7 +23,7 @@ class AudioChopperDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFre
self.chopper.setDialFrequency(frequency)
class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequencyReceiver):
class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False):
self.parser = AprsParser()
workers = [
@ -46,7 +46,7 @@ class PacketDemodulator(SecondaryDemodulator, FixedAudioRateChain, DialFrequency
self.parser.setDialFrequency(frequency)
class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain):
class PocsagDemodulator(ServiceDemodulator):
def __init__(self):
workers = [
FmDemod(),
@ -61,3 +61,28 @@ class PocsagDemodulator(SecondaryDemodulator, FixedAudioRateChain):
def getFixedAudioRate(self) -> int:
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):
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.frequencyOffset = 0
self.shift = Shift(shiftRate)
self.shift = Shift(0.0)
self.decimation = Decimator(inputRate, outputRate)
@ -88,8 +90,15 @@ class Selector(Chain):
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 setFrequencyOffset(self, offset: int) -> None:
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:
return float(math.pow(10, db / 10))
@ -125,4 +134,27 @@ class Selector(Chain):
self.replace(2, self.bandpass)
def setInputRate(self, inputRate: int) -> None:
if inputRate == self.inputRate:
return
self.inputRate = 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.modes import Modes
from csdr.chain import Chain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain
from csdr.chain.selector import Selector
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain
from csdr.chain.selector import Selector, SecondarySelector
from csdr.chain.clientaudio import ClientAudioChain
from csdr.chain.fft import FftChain
from pycsdr.modules import Buffer, Writer
@ -25,7 +25,7 @@ class ClientDemodulatorChain(Chain):
self.sampleRate = sampleRate
self.outputRate = outputRate
self.hdOutputRate = hdOutputRate
self.selector = Selector(sampleRate, outputRate, 0.0)
self.selector = Selector(sampleRate, outputRate)
self.selector.setBandpass(-4000, 4000)
self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT)
self.audioBuffer = None
@ -41,6 +41,8 @@ class ClientDemodulatorChain(Chain):
self.secondaryFftWriter = None
self.secondaryWriter = None
self.squelchLevel = -150
self.secondarySelector = None
self.secondaryFrequencyOffset = None
super().__init__([self.selector, self.demodulator, self.clientAudioChain])
def stop(self):
@ -131,8 +133,20 @@ class ClientDemodulatorChain(Chain):
self._updateDialFrequency()
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.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())
else:
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
@ -170,10 +184,7 @@ class ClientDemodulatorChain(Chain):
if offset == self.frequencyOffset:
return
self.frequencyOffset = offset
shift = -offset / self.sampleRate
self.selector.setShiftRate(shift)
self.selector.setFrequencyOffset(offset)
self._updateDialFrequency()
def setCenterFrequency(self, frequency: int) -> None:
@ -211,6 +222,8 @@ class ClientDemodulatorChain(Chain):
if not isinstance(self.demodulator, FixedIfSampleRateChain):
self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate)
if self.secondaryDemodulator is not None:
self.secondaryDemodulator.setSampleRate(outputRate)
if not isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setClientRate(outputRate)
@ -268,6 +281,15 @@ class ClientDemodulatorChain(Chain):
# TODO
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):
"""
@ -397,8 +419,6 @@ class DspManager(SdrSourceEventClient):
self.props.wireProperty("dmr_filter", self.chain.setSlotFilter),
# TODO
# self.props.wireProperty("wfm_deemphasis_tau", self.dsp.set_wfm_deemphasis_tau),
# TODO
# self.props.wireProperty("digital_voice_codecserver", self.dsp.set_codecserver),
]
# TODO
@ -414,8 +434,7 @@ class DspManager(SdrSourceEventClient):
self.subscriptions += [
self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator),
self.props.wireProperty("digimodes_fft_size", self.chain.setSecondaryFftSize),
# TODO
# self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset),
]
self.startOnAvailable = False
@ -424,7 +443,7 @@ class DspManager(SdrSourceEventClient):
super().__init__()
def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]):
def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]) -> Optional[BaseDemodulatorChain]:
if isinstance(demod, BaseDemodulatorChain):
return demod
# 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):
return mod
# TODO add remaining modes
@ -504,7 +523,12 @@ class DspManager(SdrSourceEventClient):
elif mod == "pocsag":
from csdr.chain.digimodes import 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):
demodulator = self._getSecondaryDemodulator(mod)
@ -556,12 +580,16 @@ class DspManager(SdrSourceEventClient):
def _unpickle(self, callback):
def unpickler(data):
io = BytesIO(data.tobytes())
b = data.tobytes()
io = BytesIO(b)
try:
while True:
callback(pickle.load(io))
except EOFError:
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

View File

@ -8,8 +8,8 @@ from owrx.property import PropertyLayer, PropertyDeleted
from owrx.service.schedule import ServiceScheduler
from owrx.service.chain import ServiceDemodulatorChain
from owrx.modes import Modes, DigitalMode
from typing import Union
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, DialFrequencyReceiver
from typing import Union, Optional
from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator, DialFrequencyReceiver
from pycsdr.modules import Buffer
import logging
@ -250,12 +250,11 @@ class ServiceHandler(SdrSourceEventClient):
secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation)
center_freq = source.getProps()["center_freq"]
sampleRate = source.getProps()["samp_rate"]
shift = (center_freq - frequency) / sampleRate
bandpass = modeObject.get_bandpass()
if isinstance(secondaryDemod, DialFrequencyReceiver):
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.setReader(source.getBuffer().getReader())
@ -277,8 +276,8 @@ class ServiceHandler(SdrSourceEventClient):
return Ssb()
# TODO move this elsewhere
def _getSecondaryDemodulator(self, mod):
if isinstance(mod, SecondaryDemodulator):
def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]:
if isinstance(mod, ServiceDemodulatorChain):
return mod
# TODO add remaining modes
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.selector import Selector
from csdr.chain.demodulator import BaseDemodulatorChain, SecondaryDemodulator, FixedAudioRateChain
from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator
from pycsdr.types import Format
class ServiceDemodulatorChain(Chain):
def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: SecondaryDemodulator, sampleRate: int, shiftRate: float):
# TODO magic number... check if this edge case even exsists and change the api if possible
rate = secondaryDemod.getFixedAudioRate() if isinstance(secondaryDemod, FixedAudioRateChain) else 1200
self.selector = Selector(sampleRate, rate, shiftRate, withSquelch=False)
def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: ServiceDemodulator, sampleRate: int, frequencyOffset: int):
self.selector = Selector(sampleRate, secondaryDemod.getFixedAudioRate(), withSquelch=False)
self.selector.setFrequencyOffset(frequencyOffset)
workers = [self.selector]