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 time
import os
import signal
import threading
from functools import partial
from owrx.wsjt import Ft8Chopper
import logging
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_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_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):
if self.get_secondary_demodulator() == what:
@ -238,7 +244,8 @@ class dsp(object):
secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
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)
@ -253,7 +260,11 @@ class dsp(object):
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_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
if self.secondary_shift_pipe != None: #TODO digimodes
@ -262,7 +273,7 @@ class dsp(object):
def set_secondary_offset_freq(self, 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.flush()
@ -332,6 +343,8 @@ class dsp(object):
def get_audio_rate(self):
if self.isDigitalVoice():
return 48000
elif self.secondary_demodulator == "ft8":
return 12000
return self.get_output_rate()
def isDigitalVoice(self, demodulator = None):

View File

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

View File

@ -2633,6 +2633,7 @@ function demodulator_digital_replace(subtype)
{
case "bpsk31":
case "rtty":
case "ft8":
secondary_demod_start(subtype);
demodulator_analog_replace('usb', true);
demodulator_buttons_update();
@ -2809,6 +2810,9 @@ function secondary_demod_listbox_changed()
case "rtty":
demodulator_digital_replace('rtty');
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()