7 Commits
1.2.0 ... opus

9 changed files with 121 additions and 47 deletions

View File

@ -1,3 +1,5 @@
**unreleased**
**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
modules modules

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)

4
debian/changelog vendored
View File

@ -1,3 +1,7 @@
openwebrx (1.3.0) UNRELEASED; urgency=low
-- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 16 Jun 2022 21:47:00 +0000
openwebrx (1.2.0) bullseye jammy; urgency=low openwebrx (1.2.0) bullseye jammy; urgency=low
* Major rewrite of all demodulation components to make use of the new * Major rewrite of all demodulation components to make use of the new

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);
var newFrequency;
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; if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta; 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

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