first working nfm chain using pycsdr

This commit is contained in:
Jakob Ketterl 2021-07-19 19:04:14 +02:00
parent bb77d2ce0a
commit 5bb14a8997
5 changed files with 139 additions and 25 deletions

View File

@ -35,6 +35,9 @@ from owrx.audio.chopper import AudioChopper
from csdr.pipe import Pipe from csdr.pipe import Pipe
from csdr.chain.demodulator import DemodulatorChain
from csdr.chain.fm import Fm
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -115,6 +118,10 @@ class Dsp(DirewolfConfigSubscriber):
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_".format(tmp_dir=self.temporary_directory) self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_".format(tmp_dir=self.temporary_directory)
def chain(self, which): def chain(self, which):
if self.pycsdr_enabled and which == "nfm":
self.pycsdr_chain = DemodulatorChain(self.samp_rate, self.get_audio_rate(), 0.0, Fm(self.get_audio_rate()))
return self.pycsdr_chain
chain = ["nc -v 127.0.0.1 {nc_port}"] chain = ["nc -v 127.0.0.1 {nc_port}"]
chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"] chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
if self.decimation > 1: if self.decimation > 1:
@ -463,9 +470,6 @@ class Dsp(DirewolfConfigSubscriber):
self.samp_rate = samp_rate self.samp_rate = samp_rate
self.calculate_decimation() self.calculate_decimation()
if self.running: if self.running:
if self.pycsdr_chain is not None:
self.pycsdr_chain.setSampleRate(self.samp_rate)
else:
self.restart() self.restart()
def calculate_decimation(self): def calculate_decimation(self):
@ -580,8 +584,12 @@ class Dsp(DirewolfConfigSubscriber):
return return
self.offset_freq = offset_freq self.offset_freq = offset_freq
if self.running: if self.running:
if self.pycsdr_chain is not None and isinstance(self.pycsdr_chain, DemodulatorChain):
self.pycsdr_chain.setShiftRate(-float(self.offset_freq) / self.samp_rate)
else:
self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
def set_center_freq(self, center_freq): def set_center_freq(self, center_freq):
# dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq() # dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq()
self.center_freq = center_freq self.center_freq = center_freq
@ -596,6 +604,9 @@ class Dsp(DirewolfConfigSubscriber):
self.low_cut = low_cut self.low_cut = low_cut
self.high_cut = high_cut self.high_cut = high_cut
if self.running: if self.running:
if self.pycsdr_chain is not None and isinstance(self.pycsdr_chain, DemodulatorChain):
self.pycsdr_chain.setBandpass(low_cut, high_cut)
else:
self.pipes["bpf_pipe"].write( self.pipes["bpf_pipe"].write(
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
) )
@ -615,6 +626,9 @@ class Dsp(DirewolfConfigSubscriber):
else self.squelch_level else self.squelch_level
) )
if self.running: if self.running:
if self.pycsdr_chain is not None and isinstance(self.pycsdr_chain, DemodulatorChain):
self.pycsdr_chain.setSquelchLevel(self.convertToLinear(actual_squelch))
else:
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch))) self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
def set_unvoiced_quality(self, q): def set_unvoiced_quality(self, q):
@ -708,7 +722,13 @@ class Dsp(DirewolfConfigSubscriber):
return return
self.running = True self.running = True
command_base = " | ".join(self.chain(self.demodulator)) chain = self.chain(self.demodulator)
if self.pycsdr_enabled and isinstance(chain, DemodulatorChain):
chain.setInput(self.buffer)
self.output.send_output("audio", chain.getOutput().read)
return
command_base = " | ".join(chain)
# create control pipes for csdr # create control pipes for csdr
self.try_create_pipes(self.pipe_names, command_base) self.try_create_pipes(self.pipe_names, command_base)

View File

@ -13,6 +13,9 @@ class Chain:
self._connect(self.workers[i - 1], self.workers[i]) self._connect(self.workers[i - 1], self.workers[i])
def _connect(self, w1, w2): def _connect(self, w1, w2):
if isinstance(w1, Chain):
buffer = w1.getOutput()
else:
buffer = Buffer(w1.getOutputFormat()) buffer = Buffer(w1.getOutputFormat())
w1.setOutput(buffer) w1.setOutput(buffer)
w2.setInput(buffer) w2.setInput(buffer)
@ -21,35 +24,46 @@ class Chain:
for w in self.workers: for w in self.workers:
w.stop() w.stop()
self.setInput(None) self.setInput(None)
self.setOutput(None) if self.output is not None:
self.output.stop()
def setInput(self, buffer): def setInput(self, buffer):
if self.input == buffer: if self.input == buffer:
return return
self.input = buffer self.input = buffer
if self.workers:
self.workers[0].setInput(buffer) self.workers[0].setInput(buffer)
else:
self.output = self.input
def setOutput(self, buffer): def getOutput(self):
if self.output == buffer: if self.output is None:
return if self.workers:
if self.output is not None: lastWorker = self.workers[-1]
self.output.stop() if isinstance(lastWorker, Chain):
self.output = buffer self.output = lastWorker.getOutput()
self.workers[-1].setOutput(buffer) else:
self.output = Buffer(self.getOutputFormat())
self.workers[-1].setOutput(self.output)
else:
self.output = self.input
return self.output
def getOutputFormat(self): def getOutputFormat(self):
if self.workers:
return self.workers[-1].getOutputFormat() return self.workers[-1].getOutputFormat()
else:
return self.input.getOutputFormat()
def pump(self, write): def pump(self, write):
if self.output is None: output = self.getOutput()
self.setOutput(Buffer(self.getOutputFormat()))
def copy(): def copy():
run = True run = True
while run: while run:
data = None data = None
try: try:
data = self.output.read() data = output.read()
except ValueError: except ValueError:
pass pass
if data is None or (isinstance(data, bytes) and len(data) == 0): if data is None or (isinstance(data, bytes) and len(data) == 0):

56
csdr/chain/demodulator.py Normal file
View File

@ -0,0 +1,56 @@
from csdr.chain import Chain
from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator
from abc import ABCMeta, abstractmethod
class Demodulator(Chain, metaclass=ABCMeta):
@abstractmethod
def setLastDecimation(self, decimation: Chain):
pass
class DemodulatorChain(Chain):
def __init__(self, samp_rate: int, audioRate: int, shiftRate: float, demodulator: Demodulator):
self.shift = Shift(shiftRate)
decimation, fraction = self._getDecimation(samp_rate, audioRate)
if_samp_rate = samp_rate / decimation
transition = 0.15 * (if_samp_rate / float(samp_rate))
self.decimation = FirDecimate(decimation, transition)
bp_transition = 320.0 / if_samp_rate
self.bandpass = Bandpass(bp_transition, use_fft=True)
self.squelch = Squelch(5)
if fraction != 1.0:
demodulator.setLastDecimation(Chain(FractionalDecimator(fraction)))
workers = [
self.shift,
self.decimation,
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 _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

22
csdr/chain/fm.py Normal file
View File

@ -0,0 +1,22 @@
from csdr.chain.demodulator import Demodulator, Chain
from pycsdr.modules import FmDemod, Limit, NfmDeemphasis, Agc, Convert
from pycsdr.types import Format
class Fm(Demodulator):
def __init__(self, sampleRate: int):
workers = [
FmDemod(),
Limit(),
# empty chain as placeholder for the "last decimation"
Chain(),
NfmDeemphasis(sampleRate),
Agc(Format.FLOAT),
Convert(Format.FLOAT, Format.SHORT),
]
super().__init__(*workers)
def setLastDecimation(self, decimation: Chain):
# TODO: build api to replace workers
# TODO: replace placeholder
pass

View File

@ -167,6 +167,7 @@ class DspManager(Output, SdrSourceEventClient):
def start(self): def start(self):
if self.sdrSource.isAvailable(): if self.sdrSource.isAvailable():
self.dsp.setBuffer(self.sdrSource.getBuffer())
self.dsp.start() self.dsp.start()
else: else:
self.startOnAvailable = True self.startOnAvailable = True
@ -209,6 +210,7 @@ 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.dsp.start() self.dsp.start()
self.startOnAvailable = False self.startOnAvailable = False
elif state is SdrSourceState.STOPPING: elif state is SdrSourceState.STOPPING: