first steps at rewiring the dsp stuff
This commit is contained in:
@ -1,12 +1,13 @@
|
||||
from pycsdr.modules import Buffer, Writer
|
||||
from pycsdr.modules import Buffer
|
||||
from typing import Union, Callable
|
||||
|
||||
|
||||
class Chain:
|
||||
def __init__(self, *workers):
|
||||
def __init__(self, workers):
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.clientReader = None
|
||||
self.workers = list(workers)
|
||||
self.workers = workers
|
||||
for i in range(1, len(self.workers)):
|
||||
self._connect(self.workers[i - 1], self.workers[i])
|
||||
|
||||
@ -29,19 +30,17 @@ class Chain:
|
||||
if self.workers:
|
||||
self.workers[-1].setWriter(writer)
|
||||
|
||||
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 indexOf(self, search: Union[Callable, object]) -> int:
|
||||
def searchFn(x):
|
||||
if callable(search):
|
||||
return search(x)
|
||||
else:
|
||||
return x is search
|
||||
|
||||
def getOutputFormat(self):
|
||||
if self.workers:
|
||||
return self.workers[-1].getOutputFormat()
|
||||
else:
|
||||
raise BufferError("getOutputFormat on empty chain")
|
||||
try:
|
||||
return next(i for i, v in enumerate(self.workers) if searchFn(v))
|
||||
except StopIteration:
|
||||
return -1
|
||||
|
||||
def replace(self, index, newWorker):
|
||||
if index >= len(self.workers):
|
||||
@ -55,18 +54,59 @@ class Chain:
|
||||
newWorker.setReader(self.reader)
|
||||
else:
|
||||
previousWorker = self.workers[index - 1]
|
||||
buffer = Buffer(previousWorker.getOutputFormat())
|
||||
previousWorker.setWriter(buffer)
|
||||
newWorker.setReader(buffer.getReader())
|
||||
self._connect(previousWorker, newWorker)
|
||||
|
||||
if index == len(self.workers) - 1:
|
||||
if self.writer is not None:
|
||||
newWorker.setWriter(self.writer)
|
||||
else:
|
||||
nextWorker = self.workers[index + 1]
|
||||
buffer = Buffer(newWorker.getOutputFormat())
|
||||
newWorker.setWriter(buffer)
|
||||
nextWorker.setReader(buffer.getReader())
|
||||
self._connect(newWorker, nextWorker)
|
||||
|
||||
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):
|
||||
if self.writer is None:
|
||||
@ -87,4 +127,3 @@ class Chain:
|
||||
write(data)
|
||||
|
||||
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):
|
||||
def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str):
|
||||
workers = []
|
||||
self.inputRate = inputRate
|
||||
self.clientRate = clientRate
|
||||
if inputRate != clientRate:
|
||||
# we only have an audio resampler for float ATM so if we need to resample, we need to convert
|
||||
if format != Format.FLOAT:
|
||||
@ -15,4 +17,24 @@ class ClientAudioChain(Chain):
|
||||
workers += [Convert(format, Format.SHORT)]
|
||||
if compression == "adpcm":
|
||||
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 pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer
|
||||
from pycsdr.types import Format
|
||||
from csdr.chain.digiham import Dmr
|
||||
|
||||
|
||||
class DemodulatorChain(Chain):
|
||||
def __init__(self, samp_rate: int, audioRate: int, shiftRate: float, demodulator: Chain):
|
||||
self.demodulator = demodulator
|
||||
class BaseDemodulatorChain(Chain):
|
||||
def getFixedIfSampleRate(self):
|
||||
return None
|
||||
|
||||
self.shift = Shift(shiftRate)
|
||||
def getFixedAudioRate(self):
|
||||
return None
|
||||
|
||||
decimation, fraction = self._getDecimation(samp_rate, audioRate)
|
||||
if_samp_rate = samp_rate / decimation
|
||||
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
|
||||
def supportsSquelch(self):
|
||||
return True
|
||||
|
@ -1,11 +1,11 @@
|
||||
from csdr.chain import Chain
|
||||
from csdr.chain.demodulator import BaseDemodulatorChain
|
||||
from pycsdr.modules import FmDemod, Agc, Writer
|
||||
from pycsdr.types import Format
|
||||
from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder
|
||||
from digiham.ambe import Modes
|
||||
|
||||
|
||||
class DigihamChain(Chain):
|
||||
class DigihamChain(BaseDemodulatorChain):
|
||||
def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""):
|
||||
self.decoder = decoder
|
||||
if codecserver is None:
|
||||
@ -23,11 +23,20 @@ class DigihamChain(Chain):
|
||||
DigitalVoiceFilter(),
|
||||
agc
|
||||
]
|
||||
super().__init__(*workers)
|
||||
super().__init__(workers)
|
||||
|
||||
def getFixedIfSampleRate(self):
|
||||
return 48000
|
||||
|
||||
def getFixedAudioRate(self):
|
||||
return 8000
|
||||
|
||||
def setMetaWriter(self, writer: Writer):
|
||||
self.decoder.setMetaWriter(writer)
|
||||
|
||||
def supportsSquelch(self):
|
||||
return False
|
||||
|
||||
|
||||
class Dstar(DigihamChain):
|
||||
def __init__(self, codecserver: str = ""):
|
||||
|
@ -7,7 +7,7 @@ class FftAverager(Chain):
|
||||
self.fftSize = fft_size
|
||||
self.fftAverages = fft_averages
|
||||
workers = [self._getWorker()]
|
||||
super().__init__(*workers)
|
||||
super().__init__(workers)
|
||||
|
||||
def setFftAverages(self, fft_averages):
|
||||
if self.fftAverages == fft_averages:
|
||||
@ -46,7 +46,7 @@ class FftChain(Chain):
|
||||
|
||||
self._updateParameters()
|
||||
|
||||
super().__init__(*workers)
|
||||
super().__init__(workers)
|
||||
|
||||
def _setBlockSize(self, 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)
|
Reference in New Issue
Block a user