first stab at ft8 decoding: chop up audio, call jt9 binary to decode
This commit is contained in:
parent
3f05565b7b
commit
284646ee6c
21
csdr.py
21
csdr.py
@ -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):
|
||||||
|
@ -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">
|
||||||
|
@ -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
79
owrx/wsjt.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user