7 Commits

14 changed files with 126 additions and 88 deletions

View File

@ -1,6 +1,4 @@
**1.2.1** **unreleased**
- FifiSDR support fixed (pipeline formats now line up correctly)
- Added "Device" input for FifiSDR devices for sound card selection
**1.2.0** **1.2.0**
- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator - Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator

View File

@ -1,5 +1,5 @@
from csdr.chain import Chain from csdr.chain import Chain
from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, Limit from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, OpusEncoder, Limit
from pycsdr.types import Format from pycsdr.types import Format
@ -27,10 +27,12 @@ class ClientAudioChain(Chain):
workers += [converter] workers += [converter]
if compression == "adpcm": if compression == "adpcm":
workers += [AdpcmEncoder(sync=True)] workers += [AdpcmEncoder(sync=True)]
elif compression == "opus":
workers += [OpusEncoder()]
super().__init__(workers) super().__init__(workers)
def _buildConverter(self): def _buildConverter(self):
return Converter(self.format, self.inputRate, self.clientRate) return Converter(self.format, self.inputRate, 12000)
def _updateConverter(self): def _updateConverter(self):
converter = self._buildConverter() converter = self._buildConverter()
@ -63,10 +65,18 @@ class ClientAudioChain(Chain):
self._updateConverter() self._updateConverter()
def setAudioCompression(self, compression: str) -> None: def setAudioCompression(self, compression: str) -> None:
index = self.indexOf(lambda x: isinstance(x, AdpcmEncoder)) index = self.indexOf(lambda x: isinstance(x, AdpcmEncoder) or isinstance(x, OpusEncoder))
newEncoder = None
if compression == "adpcm": if compression == "adpcm":
newEncoder = AdpcmEncoder(sync=True)
elif compression == "opus":
newEncoder = OpusEncoder()
if newEncoder:
if index < 0: if index < 0:
self.append(AdpcmEncoder(sync=True)) self.append(newEncoder)
else:
self.replace(index, newEncoder)
else: else:
if index >= 0: if index >= 0:
self.remove(index) self.remove(index)

7
debian/changelog vendored
View File

@ -1,9 +1,6 @@
openwebrx (1.2.1) bullseye jammy; urgency=low openwebrx (1.3.0) UNRELEASED; urgency=low
* FifiSDR support fixed (pipeline formats now line up correctly) -- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 16 Jun 2022 21:47:00 +0000
* Added "Device" input for FifiSDR devices for sound card selection
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 20 Sep 2022 16:01:00 +0000
openwebrx (1.2.0) bullseye jammy; urgency=low openwebrx (1.2.0) bullseye jammy; urgency=low

View File

@ -29,7 +29,7 @@ tar xfz $PACKAGE
git clone https://github.com/jancona/hpsdrconnector.git git clone https://github.com/jancona/hpsdrconnector.git
pushd hpsdrconnector pushd hpsdrconnector
git checkout v0.6.1 git checkout v0.6.0
/tmp/go/bin/go build /tmp/go/bin/go build
install -m 0755 hpsdrconnector /usr/local/bin install -m 0755 hpsdrconnector /usr/local/bin

View File

@ -31,11 +31,11 @@ popd
rm -rf js8py rm -rf js8py
git clone https://github.com/jketterl/csdr.git git clone https://github.com/jketterl/csdr.git
cmakebuild csdr 0.18.1 cmakebuild csdr 0.18.0
git clone https://github.com/jketterl/pycsdr.git git clone https://github.com/jketterl/pycsdr.git
cd pycsdr cd pycsdr
git checkout 0.18.1 git checkout 0.18.0
./setup.py install install_headers ./setup.py install install_headers
cd .. cd ..
rm -rf pycsdr rm -rf pycsdr

View File

@ -55,6 +55,10 @@
</div> </div>
<div id="openwebrx-panels-container"> <div id="openwebrx-panels-container">
<div id="openwebrx-panels-container-left"> <div id="openwebrx-panels-container-left">
<div class="openwebrx-panel" data-panel-name="client-under-devel" style="width: 245px; background-color: Red;">
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
<br />We're working on the code right now, so the application might fail.
</div>
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes"> <div class="openwebrx-panel" id="openwebrx-panel-digimodes" style="display: none; width: 619px;" data-panel-name="digimodes">
<div id="openwebrx-digimode-canvas-container"> <div id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div> <div id="openwebrx-digimode-select-channel"></div>

View File

@ -21,7 +21,15 @@ function AudioEngine(maxBufferLength, audioReporter) {
me._start(); me._start();
} }
this.audioCodec = new ImaAdpcmCodec(); var onAudio = function(audioData) {
me.playbackAudio(audioData);
}
this.audioCodecs = {
"adpcm": new ImaAdpcmCodec(onAudio),
"opus": new OpusCodec(onAudio)
};
this.compression = 'none'; this.compression = 'none';
this.setupResampling(); this.setupResampling();
@ -279,24 +287,26 @@ AudioEngine.prototype.getSampleRate = function() {
AudioEngine.prototype.processAudio = function(data, resampler) { AudioEngine.prototype.processAudio = function(data, resampler) {
if (!this.audioNode) return; if (!this.audioNode) return;
this.audioBytes.add(data.byteLength); this.audioBytes.add(data.byteLength);
var buffer;
if (this.compression === "adpcm") { if (this.compression !== "none") {
//resampling & ADPCM this.audioCodecs[this.compression].decodeAsync(new Uint8Array(data));
buffer = this.audioCodec.decodeWithSync(new Uint8Array(data));
} else { } else {
buffer = new Int16Array(data); this.playbackAudio(new Int16Array(data));
} }
buffer = resampler.process(buffer); }
AudioEngine.prototype.playbackAudio = function(audioData) {
//var buffer = this.resampler.process(audioData);
if (this.audioNode.port) { if (this.audioNode.port) {
// AudioWorklets supported // AudioWorklets supported
this.audioNode.port.postMessage(buffer); this.audioNode.port.postMessage(audioData);
} else { } else {
// silently drop excess samples // silently drop excess samples
if (this.getBuffersize() + buffer.length <= this.maxBufferSize) { if (this.getBuffersize() + buffer.length <= this.maxBufferSize) {
this.audioBuffers.push(buffer); this.audioBuffers.push(buffer);
} }
} }
} };
AudioEngine.prototype.pushAudio = function(data) { AudioEngine.prototype.pushAudio = function(data) {
this.processAudio(data, this.resampler); this.processAudio(data, this.resampler);
@ -320,8 +330,9 @@ AudioEngine.prototype.getBuffersize = function() {
return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0);
}; };
function ImaAdpcmCodec() { function ImaAdpcmCodec(callback) {
this.reset(); this.reset();
this.callback = callback;
} }
ImaAdpcmCodec.prototype.reset = function() { ImaAdpcmCodec.prototype.reset = function() {
@ -357,7 +368,7 @@ ImaAdpcmCodec.prototype.decode = function(data) {
return output; return output;
}; };
ImaAdpcmCodec.prototype.decodeWithSync = function(data) { ImaAdpcmCodec.prototype.decodeAsync = function(data, callback) {
var output = new Int16Array(data.length * 2); var output = new Int16Array(data.length * 2);
var index = this.skip; var index = this.skip;
var oi = 0; var oi = 0;
@ -391,7 +402,7 @@ ImaAdpcmCodec.prototype.decodeWithSync = function(data) {
} }
} }
this.skip = index - data.length; this.skip = index - data.length;
return output.slice(0, oi); this.callback(output.slice(0, oi));
}; };
ImaAdpcmCodec.prototype.decodeNibble = function(nibble) { ImaAdpcmCodec.prototype.decodeNibble = function(nibble) {
@ -412,6 +423,39 @@ ImaAdpcmCodec.prototype.decodeNibble = function(nibble) {
return this.predictor; return this.predictor;
}; };
function OpusCodec(callback) {
this.callback = callback;
this.resetDecoder();
}
OpusCodec.prototype.resetDecoder = function() {
var me = this;
me.decoder = new AudioDecoder({
output: function(audioData) {
var buffer = new Float32Array(audioData.numberOfFrames * audioData.numberOfChannels);
audioData.copyTo(buffer, {planeIndex: 0});
me.callback(buffer);
},
error: function(e) {
console.error(e);
me.resetDecoder();
}
});
me.decoder.configure({
codec: "opus",
sampleRate: 12000,
numberOfChannels: 1
});
}
OpusCodec.prototype.decodeAsync = function(data) {
this.decoder.decode(new EncodedAudioChunk({
type: "key",
data: data,
timestamp: 0
}));
};
function Interpolator(factor) { function Interpolator(factor) {
this.factor = factor; this.factor = factor;
this.lowpass = new Lowpass(factor) this.lowpass = new Lowpass(factor)

View File

@ -98,8 +98,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (index < 0) return; if (index < 0) return;
var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index); var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index);
if (e.originalEvent.deltaY > 0) delta *= -1; var newFrequency;
var newFrequency = me.frequency + delta; if ('deltaMode' in e.originalEvent && e.originalEvent.deltaMode === 0) {
newFrequency = me.frequency - delta * (e.originalEvent.deltaY / 50);
} else {
if (e.originalEvent.deltaY > 0) delta *= -1;
newFrequency = me.frequency + delta;
}
me.element.trigger('frequencychange', newFrequency); me.element.trigger('frequencychange', newFrequency);
}); });

View File

@ -60,7 +60,7 @@ function zoomOutOneStep() {
} }
function zoomInTotal() { function zoomInTotal() {
zoom_set(zoom_levels.length - 1); zoom_set(zoom_levels_count);
} }
function zoomOutTotal() { function zoomOutTotal() {
@ -317,7 +317,7 @@ function scale_px_from_freq(f, range) {
function get_visible_freq_range() { function get_visible_freq_range() {
if (!bandwidth) return false; if (!bandwidth) return false;
var fcalc = function (x) { var fcalc = function (x) {
var canvasWidth = waterfallWidth() * zoom_levels[zoom_level]; var canvasWidth = waterfallWidth() * get_zoom(zoom_level);
return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2); return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2);
}; };
var out = { var out = {
@ -565,7 +565,7 @@ function canvas_mousemove(evt) {
) { ) {
zoom_center_rel += dpx; zoom_center_rel += dpx;
} }
resize_canvases(false); resize_canvases();
canvas_drag_last_x = evt.pageX; canvas_drag_last_x = evt.pageX;
canvas_drag_last_y = evt.pageY; canvas_drag_last_y = evt.pageY;
mkscale(); mkscale();
@ -616,9 +616,14 @@ function get_relative_x(evt) {
function canvas_mousewheel(evt) { function canvas_mousewheel(evt) {
if (!waterfall_setup_done) return; if (!waterfall_setup_done) return;
var delta = -evt.deltaY;
// deltaMode 0 means pixels instead of lines
if ('deltaMode' in evt && evt.deltaMode === 0) {
delta /= 50;
}
var relativeX = get_relative_x(evt); var relativeX = get_relative_x(evt);
var dir = (evt.deltaY / Math.abs(evt.deltaY)) > 0; zoom_step(delta, relativeX, zoom_center_where_calc(evt.pageX));
zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX));
evt.preventDefault(); evt.preventDefault();
} }
@ -631,7 +636,6 @@ function get_zoom_coeff_from_hps(hps) {
return bandwidth / shown_bw; return bandwidth / shown_bw;
} }
var zoom_levels = [1];
var zoom_level = 0; var zoom_level = 0;
var zoom_offset_px = 0; var zoom_offset_px = 0;
var zoom_center_rel = 0; var zoom_center_rel = 0;
@ -639,45 +643,48 @@ var zoom_center_where = 0;
var smeter_level = 0; var smeter_level = 0;
function mkzoomlevels() { function get_zoom(level) {
zoom_levels = [1];
var maxc = get_zoom_coeff_from_hps(zoom_max_level_hps); var maxc = get_zoom_coeff_from_hps(zoom_max_level_hps);
if (maxc < 1) return; if (maxc < 1) return;
// logarithmic interpolation // logarithmic interpolation
var zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count); var zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count);
for (var i = 1; i < zoom_levels_count; i++) return Math.pow(zoom_ratio, level);
zoom_levels.push(Math.pow(zoom_ratio, i));
} }
function zoom_step(out, where, onscreen) { function zoom_step(delta, where, onscreen) {
if ((out && zoom_level === 0) || (!out && zoom_level >= zoom_levels_count - 1)) return; zoom_level += delta;
if (out) --zoom_level; if (zoom_level < 0) {
else ++zoom_level; zoom_level = 0;
} else if (zoom_level > zoom_levels_count) {
zoom_level = zoom_levels_count;
}
zoom_center_rel = canvas_get_freq_offset(where); zoom_center_rel = canvas_get_freq_offset(where);
//console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString());
zoom_center_where = onscreen; zoom_center_where = onscreen;
//console.log(zoom_center_where, zoom_center_rel, where); resize_canvases();
resize_canvases(true);
mkscale(); mkscale();
bookmarks.position(); bookmarks.position();
} }
function zoom_set(level) { function zoom_set(level) {
if (!(level >= 0 && level <= zoom_levels.length - 1)) return; if (level < 0) {
level = parseInt(level); zoom_level = 0;
zoom_level = level; } else if (level > zoom_levels_count) {
zoom_level = zoom_levels_count;
} else {
zoom_level = parseFloat(level);
}
//zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope
zoom_center_rel = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency(); zoom_center_rel = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency();
zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack
resize_canvases(true); resize_canvases();
mkscale(); mkscale();
bookmarks.position(); bookmarks.position();
} }
function zoom_calc() { function zoom_calc() {
var winsize = waterfallWidth(); var winsize = waterfallWidth();
var canvases_new_width = winsize * zoom_levels[zoom_level]; var canvases_new_width = winsize * get_zoom(zoom_level);
zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where)); zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where));
if (zoom_offset_px > 0) zoom_offset_px = 0; if (zoom_offset_px > 0) zoom_offset_px = 0;
if (zoom_offset_px < winsize - canvases_new_width) if (zoom_offset_px < winsize - canvases_new_width)
@ -747,7 +754,7 @@ function on_ws_recv(evt) {
if ('audio_compression' in config) { if ('audio_compression' in config) {
var audio_compression = config['audio_compression']; var audio_compression = config['audio_compression'];
audioEngine.setCompression(audio_compression); audioEngine.setCompression(audio_compression);
divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); divlog("Audio stream is " + ((audio_compression !== "none") ? "compressed" : "uncompressed") + ".");
} }
if ('fft_compression' in config) { if ('fft_compression' in config) {
fft_compression = config['fft_compression']; fft_compression = config['fft_compression'];
@ -1122,12 +1129,10 @@ function shift_canvases() {
canvas_maxshift++; canvas_maxshift++;
} }
function resize_canvases(zoom) { function resize_canvases() {
if (typeof zoom === "undefined") zoom = false;
if (!zoom) mkzoomlevels();
zoom_calc(); zoom_calc();
$('#webrx-canvas-container').css({ $('#webrx-canvas-container').css({
width: waterfallWidth() * zoom_levels[zoom_level] + 'px', width: waterfallWidth() * get_zoom(zoom_level) + 'px',
left: zoom_offset_px + "px" left: zoom_offset_px + "px"
}); });
} }
@ -1136,7 +1141,6 @@ function waterfall_init() {
init_canvas_container(); init_canvas_container();
resize_canvases(); resize_canvases();
scale_setup(); scale_setup();
mkzoomlevels();
waterfall_setup_done = 1; waterfall_setup_done = 1;
} }

View File

@ -132,6 +132,7 @@ class GeneralSettingsController(SettingsFormController):
"Audio compression", "Audio compression",
options=[ options=[
Option("adpcm", "ADPCM"), Option("adpcm", "ADPCM"),
Option("opus", "OPUS"),
Option("none", "None"), Option("none", "None"),
], ],
), ),

View File

@ -249,13 +249,10 @@ class SdrSource(ABC):
def getPort(self): def getPort(self):
return self.port return self.port
def _getTcpSourceFormat(self):
return Format.COMPLEX_FLOAT
def _getTcpSource(self): def _getTcpSource(self):
with self.modificationLock: with self.modificationLock:
if self.tcpSource is None: if self.tcpSource is None:
self.tcpSource = TcpSource(self.port, self._getTcpSourceFormat()) self.tcpSource = TcpSource(self.port, Format.COMPLEX_FLOAT)
return self.tcpSource return self.tcpSource
def getBuffer(self): def getBuffer(self):

View File

@ -11,10 +11,6 @@ logger = logging.getLogger(__name__)
class DirectSource(SdrSource, metaclass=ABCMeta): class DirectSource(SdrSource, metaclass=ABCMeta):
def __init__(self, id, props):
self._conversion = None
super().__init__(id, props)
def onPropertyChange(self, changes): def onPropertyChange(self, changes):
logger.debug("restarting sdr source due to property changes: {0}".format(changes)) logger.debug("restarting sdr source due to property changes: {0}".format(changes))
self.stop() self.stop()
@ -52,10 +48,6 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
def getFormatConversion(self) -> Optional[Chain]: def getFormatConversion(self) -> Optional[Chain]:
return None return None
def _getTcpSourceFormat(self):
conversion = self.getFormatConversion()
return Format.COMPLEX_FLOAT if conversion is None else conversion.getInputFormat()
# override this in subclasses, if necessary # override this in subclasses, if necessary
def sleepOnRestart(self): def sleepOnRestart(self):
pass pass
@ -65,12 +57,12 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
source = self._getTcpSource() source = self._getTcpSource()
buffer = Buffer(source.getOutputFormat()) buffer = Buffer(source.getOutputFormat())
source.setWriter(buffer) source.setWriter(buffer)
self._conversion = self.getFormatConversion() conversion = self.getFormatConversion()
if self._conversion is not None: if conversion is not None:
self._conversion.setReader(buffer.getReader()) conversion.setReader(buffer.getReader())
# this one must be COMPLEX_FLOAT # this one must be COMPLEX_FLOAT
buffer = Buffer(Format.COMPLEX_FLOAT) buffer = Buffer(Format.COMPLEX_FLOAT)
self._conversion.setWriter(buffer) conversion.setWriter(buffer)
self.buffer = buffer self.buffer = buffer
return self.buffer return self.buffer

View File

@ -4,8 +4,6 @@ from subprocess import Popen
from csdr.chain import Chain from csdr.chain import Chain
from pycsdr.modules import Convert, Gain from pycsdr.modules import Convert, Gain
from pycsdr.types import Format from pycsdr.types import Format
from typing import List
from owrx.form.input import Input, TextInput
import logging import logging
@ -51,15 +49,3 @@ class FifiSdrDeviceDescription(DirectSourceDeviceDescription):
def supportsPpm(self): def supportsPpm(self):
# not currently mapped, and it's unclear how this should be sent to the device # not currently mapped, and it's unclear how this should be sent to the device
return False return False
def getInputs(self) -> List[Input]:
return super().getInputs() + [
TextInput(
"device",
"Device identifier",
infotext="Alsa audio device identifier",
),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["device"]

View File

@ -1,5 +1,5 @@
from distutils.version import LooseVersion from distutils.version import LooseVersion
_versionstring = "1.2.1" _versionstring = "1.3.0-dev"
looseversion = LooseVersion(_versionstring) looseversion = LooseVersion(_versionstring)
openwebrx_version = "v{0}".format(looseversion) openwebrx_version = "v{0}".format(looseversion)