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**
- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator
modules

View File

@ -1,5 +1,5 @@
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
@ -27,10 +27,12 @@ class ClientAudioChain(Chain):
workers += [converter]
if compression == "adpcm":
workers += [AdpcmEncoder(sync=True)]
elif compression == "opus":
workers += [OpusEncoder()]
super().__init__(workers)
def _buildConverter(self):
return Converter(self.format, self.inputRate, self.clientRate)
return Converter(self.format, self.inputRate, 12000)
def _updateConverter(self):
converter = self._buildConverter()
@ -63,10 +65,18 @@ class ClientAudioChain(Chain):
self._updateConverter()
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":
newEncoder = AdpcmEncoder(sync=True)
elif compression == "opus":
newEncoder = OpusEncoder()
if newEncoder:
if index < 0:
self.append(AdpcmEncoder(sync=True))
self.append(newEncoder)
else:
self.replace(index, newEncoder)
else:
if index >= 0:
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
* Major rewrite of all demodulation components to make use of the new

View File

@ -55,6 +55,10 @@
</div>
<div id="openwebrx-panels-container">
<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 id="openwebrx-digimode-canvas-container">
<div id="openwebrx-digimode-select-channel"></div>

View File

@ -21,7 +21,15 @@ function AudioEngine(maxBufferLength, audioReporter) {
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.setupResampling();
@ -279,24 +287,26 @@ AudioEngine.prototype.getSampleRate = function() {
AudioEngine.prototype.processAudio = function(data, resampler) {
if (!this.audioNode) return;
this.audioBytes.add(data.byteLength);
var buffer;
if (this.compression === "adpcm") {
//resampling & ADPCM
buffer = this.audioCodec.decodeWithSync(new Uint8Array(data));
if (this.compression !== "none") {
this.audioCodecs[this.compression].decodeAsync(new Uint8Array(data));
} 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) {
// AudioWorklets supported
this.audioNode.port.postMessage(buffer);
this.audioNode.port.postMessage(audioData);
} else {
// silently drop excess samples
if (this.getBuffersize() + buffer.length <= this.maxBufferSize) {
this.audioBuffers.push(buffer);
}
}
}
};
AudioEngine.prototype.pushAudio = function(data) {
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);
};
function ImaAdpcmCodec() {
function ImaAdpcmCodec(callback) {
this.reset();
this.callback = callback;
}
ImaAdpcmCodec.prototype.reset = function() {
@ -357,7 +368,7 @@ ImaAdpcmCodec.prototype.decode = function(data) {
return output;
};
ImaAdpcmCodec.prototype.decodeWithSync = function(data) {
ImaAdpcmCodec.prototype.decodeAsync = function(data, callback) {
var output = new Int16Array(data.length * 2);
var index = this.skip;
var oi = 0;
@ -391,7 +402,7 @@ ImaAdpcmCodec.prototype.decodeWithSync = function(data) {
}
}
this.skip = index - data.length;
return output.slice(0, oi);
this.callback(output.slice(0, oi));
};
ImaAdpcmCodec.prototype.decodeNibble = function(nibble) {
@ -412,6 +423,39 @@ ImaAdpcmCodec.prototype.decodeNibble = function(nibble) {
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) {
this.factor = factor;
this.lowpass = new Lowpass(factor)

View File

@ -98,8 +98,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (index < 0) return;
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;
var newFrequency = me.frequency + delta;
newFrequency = me.frequency + delta;
}
me.element.trigger('frequencychange', newFrequency);
});

View File

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

View File

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

View File

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