first stab at ft8 decoding: chop up audio, call jt9 binary to decode

This commit is contained in:
Jakob Ketterl 2019-07-06 18:21:43 +02:00
parent 3f05565b7b
commit 284646ee6c
4 changed files with 101 additions and 4 deletions

21
csdr.py
View File

@ -21,11 +21,11 @@ OpenWebRX csdr plugin: do the signal processing with csdr
""" """
import subprocess import subprocess
import time
import os import os
import signal import signal
import threading import threading
from functools import partial from functools import partial
from owrx.wsjt import Ft8Chopper
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -186,6 +186,12 @@ class dsp(object):
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8"
elif which == "ft8":
chain = secondary_chain_base + "csdr realpart_cf | "
if self.last_decimation != 1.0 :
chain += "csdr fractional_decimator_ff {last_decimation} | "
chain += "csdr agc_ff | csdr limit_ff | csdr convert_f_s16"
return chain
def set_secondary_demodulator(self, what): def set_secondary_demodulator(self, what):
if self.get_secondary_demodulator() == what: if self.get_secondary_demodulator() == what:
@ -238,7 +244,8 @@ class dsp(object):
secondary_samples_per_bits=self.secondary_samples_per_bits(), secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
if_samp_rate=self.if_samp_rate() if_samp_rate=self.if_samp_rate(),
last_decimation=self.last_decimation
) )
logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft) logger.debug("[openwebrx-dsp-plugin:csdr] secondary command (fft) = %s", secondary_command_fft)
@ -253,7 +260,11 @@ class dsp(object):
self.secondary_processes_running = True self.secondary_processes_running = True
self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read()))) self.output.add_output("secondary_fft", partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())))
self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) if self.get_secondary_demodulator() == "ft8":
chopper = Ft8Chopper(self.secondary_process_demod.stdout)
chopper.start()
else:
self.output.add_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
#open control pipes for csdr and send initialization data #open control pipes for csdr and send initialization data
if self.secondary_shift_pipe != None: #TODO digimodes if self.secondary_shift_pipe != None: #TODO digimodes
@ -262,7 +273,7 @@ class dsp(object):
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq=value self.secondary_offset_freq=value
if self.secondary_processes_running: if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate()))
self.secondary_shift_pipe_file.flush() self.secondary_shift_pipe_file.flush()
@ -332,6 +343,8 @@ class dsp(object):
def get_audio_rate(self): def get_audio_rate(self):
if self.isDigitalVoice(): if self.isDigitalVoice():
return 48000 return 48000
elif self.secondary_demodulator == "ft8":
return 12000
return self.get_output_rate() return self.get_output_rate()
def isDigitalVoice(self, demodulator = None): def isDigitalVoice(self, demodulator = None):

View File

@ -109,6 +109,7 @@
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();"> <select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option> <option value="none"></option>
<option value="bpsk31">BPSK31</option> <option value="bpsk31">BPSK31</option>
<option value="ft8">FT8</option>
</select> </select>
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">

View File

@ -2633,6 +2633,7 @@ function demodulator_digital_replace(subtype)
{ {
case "bpsk31": case "bpsk31":
case "rtty": case "rtty":
case "ft8":
secondary_demod_start(subtype); secondary_demod_start(subtype);
demodulator_analog_replace('usb', true); demodulator_analog_replace('usb', true);
demodulator_buttons_update(); demodulator_buttons_update();
@ -2809,6 +2810,9 @@ function secondary_demod_listbox_changed()
case "rtty": case "rtty":
demodulator_digital_replace('rtty'); demodulator_digital_replace('rtty');
break; break;
case "ft8":
demodulator_digital_replace('ft8');
break;
} }
} }

79
owrx/wsjt.py Normal file
View File

@ -0,0 +1,79 @@
import threading
import wave
from datetime import datetime, timedelta
import time
import sched
import subprocess
import logging
logger = logging.getLogger(__name__)
class Ft8Chopper(threading.Thread):
def __init__(self, source):
self.source = source
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.scheduler = sched.scheduler(time.time, time.sleep)
self.queue = []
self.doRun = True
super().__init__()
def getWaveFile(self):
filename = "/tmp/openwebrx-ft8chopper-{0}.wav".format(datetime.now().strftime("%Y%m%d-%H%M%S"))
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
wavefile.setsampwidth(2)
wavefile.setframerate(12000)
return (filename, wavefile)
def getNextDecodingTime(self):
t = datetime.now()
seconds = (int(t.second / 15) + 1) * 15
if seconds >= 60:
t = t + timedelta(minutes = 1)
seconds = 0
t = t.replace(second = seconds, microsecond = 0)
logger.debug("scheduling: {0}".format(t))
return t.timestamp()
def startScheduler(self):
self._scheduleNextSwitch()
threading.Thread(target = self.scheduler.run).start()
def emptyScheduler(self):
for event in self.scheduler.queue:
self.scheduler.cancel(event)
def _scheduleNextSwitch(self):
self.scheduler.enterabs(self.getNextDecodingTime(), 1, self.switchFiles)
def switchFiles(self):
file = self.wavefile
filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile()
file.close()
self.queue.append(filename)
self._scheduleNextSwitch()
def decode(self):
if self.queue:
file = self.queue.pop()
logger.debug("processing file {0}".format(file))
#TODO expose decoding quality parameters through config
self.decoder = subprocess.Popen(["jt9", "--ft8", "-d", "3", file])
def run(self) -> None:
logger.debug("FT8 chopper starting up")
self.startScheduler()
while self.doRun:
data = self.source.read(256)
if data is None or (isinstance(data, bytes) and len(data) == 0):
logger.warning("zero read on ft8 chopper")
self.doRun = False
else:
self.wavefile.writeframes(data)
self.decode()
logger.debug("FT8 chopper shutting down")
self.emptyScheduler()