first steps at rewiring the dsp stuff
This commit is contained in:
parent
0f1feb9d47
commit
5032f4b66d
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
self.clientReader.stop()
|
|
||||||
self.clientReader = None
|
|
||||||
|
|
||||||
def getOutputFormat(self):
|
|
||||||
if self.workers:
|
|
||||||
return self.workers[-1].getOutputFormat()
|
|
||||||
else:
|
else:
|
||||||
raise BufferError("getOutputFormat on empty chain")
|
return x is search
|
||||||
|
|
||||||
|
try:
|
||||||
|
return next(i for i, v in enumerate(self.workers) if searchFn(v))
|
||||||
|
except StopIteration:
|
||||||
|
return -1
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -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
54
csdr/chain/analog.py
Normal 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)
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
|
@ -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 = ""):
|
||||||
|
@ -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):
|
||||||
|
@ -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
126
csdr/chain/selector.py
Normal 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)
|
@ -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)
|
|
@ -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
|
||||||
|
220
owrx/dsp.py
220
owrx/dsp.py
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user