From a5bf8e6e80ec1919e609b0fe83db8ee1186e288e Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Wed, 24 Apr 2024 20:11:39 -0400 Subject: [PATCH] Split waterfall functions from the openwebrx.js. --- htdocs/index.html | 8 +- htdocs/lib/UI.js | 9 +- htdocs/lib/Waterfall.js | 196 +++++++++++++++++++++++++++++++++++++ htdocs/openwebrx.js | 193 ++++-------------------------------- owrx/controllers/assets.py | 1 + 5 files changed, 228 insertions(+), 179 deletions(-) create mode 100644 htdocs/lib/Waterfall.js diff --git a/htdocs/index.html b/htdocs/index.html index a07726bc..c8eb32bc 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -257,21 +257,21 @@ -
+
- +
-
+
- +
▴ Settings
diff --git a/htdocs/lib/UI.js b/htdocs/lib/UI.js index 6c21e18c..6228fbad 100644 --- a/htdocs/lib/UI.js +++ b/htdocs/lib/UI.js @@ -263,20 +263,23 @@ UI.setWfTheme = function(theme) { // Set selector $('#openwebrx-wf-themes-listbox').val(theme); - // Generate new colors - waterfall_colors = chroma.scale(this.wfThemes[theme]).colors(256, 'rgb'); + // Set new colors in the waterfall + Waterfall.setTheme(this.wfThemes[theme]); }; // Set default waterfall color theme. UI.setDefaultWfTheme = function(colors) { + // Update default waterfall theme with new colors this.wfThemes['default'] = colors; + + // If default theme currently used, update waterfall if (this.wfTheme === 'default') { this.wfTheme = null; this.setWfTheme('default'); } }; -// Waterfall color schemes +// Waterfall color themes UI.wfThemes = { 'default' : [0x000000, 0xFFFFFF], 'teejeez' : [0x000000, 0x0000FF, 0x00FFFF, 0x00FF00, 0xFFFF00, 0xFF0000, 0xFF00FF, 0xFFFFFF], diff --git a/htdocs/lib/Waterfall.js b/htdocs/lib/Waterfall.js new file mode 100644 index 00000000..acaf2096 --- /dev/null +++ b/htdocs/lib/Waterfall.js @@ -0,0 +1,196 @@ +// +// Waterfall colors +// + +function Waterfall() {} + +Waterfall.colors = chroma.scale([0x000000, 0xFFFFFF]).colors(256, 'rgb'); +Waterfall.levels = { min: -150, max: 0 }; +Waterfall.fixed_levels = { min: -150, max: 0 }; +Waterfall.auto_levels = { min: -150, max: 0 }; +Waterfall.cont_levels = { min: -150, max: 0 }; +Waterfall.auto_min_range = 0; +Waterfall.measure_minmax_now = false; +Waterfall.measure_minmax_continuous = false; + +// Get current waterfall min/max levels range. +Waterfall.getRange = function() { + return this.levels; +}; + +// Set waterfall color theme, passed as an array of +// integer RGB values. +Waterfall.setTheme = function(theme) { + this.colors = chroma.scale(theme).colors(256, 'rgb'); +}; + +// Configure waterfall parameters from the attributes +// sent by the server. +Waterfall.configure = function(config) { + if ('waterfall_levels' in config) + this.fixed_levels = config['waterfall_levels']; + if ('waterfall_auto_levels' in config) + this.auto_levels = config['waterfall_auto_levels']; + if ('waterfall_auto_min_range' in config) + this.auto_min_range = config['waterfall_auto_min_range']; + if ('waterfall_auto_level_default_mode' in config) + this.toggleContinuousRange(config['waterfall_auto_level_default_mode']); +}; + +// Use one-time automatic min/max level update. +Waterfall.setAutoRange = function() { + this.measure_minmax_now = true; +}; + +// Use default min/max levels. +Waterfall.setDefaultRange = function() { + this.levels.min = this.fixed_levels.min; + this.levels.max = this.fixed_levels.max; + this.updateSliders(); + this.resetContinuousColors(); +}; + +// Enable continuous min/max level updates. +Waterfall.toggleContinuousRange = function(on) { + // If no argument given, toggle continuous mode + on = typeof(on) === 'undefined'? !this.measure_minmax_continuous : on; + + this.measure_minmax_continuous = on; + + var autoButton = $('#openwebrx-waterfall-colors-auto'); + autoButton[on ? 'addClass' : 'removeClass']('highlighted'); + $('#openwebrx-waterfall-color-min, #openwebrx-waterfall-color-max').prop('disabled', on); +}; + +// Update waterfall min/max levels from sliders. +Waterfall.updateColors = function(which) { + var $wfmax = $("#openwebrx-waterfall-color-max"); + var $wfmin = $("#openwebrx-waterfall-color-min"); + + this.levels.max = parseInt($wfmax.val()); + this.levels.min = parseInt($wfmin.val()); + + if (this.levels.min >= this.levels.max) { + if (!which) { + this.levels.min = this.levels.max -1; + } else { + this.levels.max = this.levels.min + 1; + } + } + + this.updateSliders(); +}; + +// Update waterfall level sliders from min/max levels. +Waterfall.updateSliders = function() { + $('#openwebrx-waterfall-color-max') + .val(this.levels.max) + .attr('title', 'Waterfall maximum level (' + Math.round(this.levels.max) + ' dB)'); + $('#openwebrx-waterfall-color-min') + .val(this.levels.min) + .attr('title', 'Waterfall minimum level (' + Math.round(this.levels.min) + ' dB)'); +}; + +// Update automatic min/max levels. +Waterfall.updateAutoColors = function(levels) { + var min_level = levels.min - this.auto_levels.min; + var max_level = levels.max + this.auto_levels.max; + + max_level = Math.max(min_level + (this.auto_min_range || 0), max_level); + + this.levels.min = min_level; + this.levels.max = max_level; + this.updateSliders(); +}; + +// Reset continuous min/max levels. +Waterfall.resetContinuousColors = function() { + this.cont_levels.min = this.levels.min; + this.cont_levels.max = this.levels.max; +}; + +// Update continous min/max levels. +Waterfall.updateContinuousColors = function(levels) { + if (levels.max > this.cont_levels.max + 1) { + this.cont_levels.max += 1; + } else if (levels.max < this.cont_levels.max - 1) { + this.cont_levels.max -= .1; + } + + if (levels.min < this.cont_levels.min - 1) { + this.cont_levels.min -= 1; + } else if (levels.min > this.cont_levels.min + 1) { + this.cont_levels.min += .1; + } + + this.updateAutoColors(this.cont_levels); +}; + +// Create a color based on the dB value and current color theme. +Waterfall.makeColor = function(db) { + var v = (db - this.levels.min) / (this.levels.max - this.levels.min); + v = Math.max(0, Math.min(1, v)) * (this.colors.length - 1); + var i = Math.floor(v); + v = v - i; + + if (v == 0) { + return this.colors[i]; + } else { + var c0 = this.colors[i]; + var c1 = this.colors[i+1]; + return [ + c0[0] + v * (c1[0] - c0[0]), + c0[1] + v * (c1[1] - c0[1]), + c0[2] + v * (c1[2] - c0[2]) + ]; + } +}; + +// Draw a single line of waterfall pixels based on the input data. +Waterfall.drawLine = function(out, data, offset = 0) { + var y = 0; + for (var x = 0; x < data.length; x++) { + var color = this.makeColor(data[x] + offset); + out[y++] = color[0]; + out[y++] = color[1]; + out[y++] = color[2]; + out[y++] = 255; + } +} + +// Measure min/max levels from the incoming data, if necessary. +Waterfall.measureRange = function(data) { + // Drop out unless we actually need to measure levels + if (!this.measure_minmax_now && !this.measure_minmax_continuous) return; + + // Get visible range of frequencies + var range = get_visible_freq_range(); + var start = center_freq - bandwidth / 2; + + // This is based on an oversampling factor of about 1.25 + range.start = Math.max(0.1, (range.start - start) / bandwidth); + range.end = Math.min(0.9, (range.end - start) / bandwidth); + + // Align to the range edges, do not let things flip over + if (range.start >= 0.9) + range.start = range.end - range.bw / bandwidth; + else if (range.end <= 0.1) + range.end = range.start + range.bw / bandwidth; + + // Find min/max levels within the range + data = data.slice(range.start * data.length, range.end * data.length); + var levels = { + min: Math.min.apply(Math, data), + max: Math.max.apply(Math, data) + }; + + if (this.measure_minmax_now) { + this.measure_minmax_now = false; + this.updateAutoColors(levels); + this.resetContinuousColors(); + } + + if (this.measure_minmax_continuous) { + this.updateContinuousColors(levels); + } +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 5ee0c306..47574c66 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -119,80 +119,6 @@ function jumpBySteps(steps) { } } -var waterfall_min_level; -var waterfall_max_level; -var waterfall_min_level_default; -var waterfall_max_level_default; -var waterfall_colors; -var waterfall_auto_levels; -var waterfall_auto_min_range; -var waterfall_measure_minmax_now = false; -var waterfall_measure_minmax_continuous = false; - -function updateWaterfallColors(which) { - var $wfmax = $("#openwebrx-waterfall-color-max"); - var $wfmin = $("#openwebrx-waterfall-color-min"); - waterfall_max_level = parseInt($wfmax.val()); - waterfall_min_level = parseInt($wfmin.val()); - if (waterfall_min_level >= waterfall_max_level) { - if (!which) { - waterfall_min_level = waterfall_max_level -1; - } else { - waterfall_max_level = waterfall_min_level + 1; - } - } - updateWaterfallSliders(); -} - -function updateWaterfallSliders() { - $('#openwebrx-waterfall-color-max') - .val(waterfall_max_level) - .attr('title', 'Waterfall maximum level (' + Math.round(waterfall_max_level) + ' dB)'); - $('#openwebrx-waterfall-color-min') - .val(waterfall_min_level) - .attr('title', 'Waterfall minimum level (' + Math.round(waterfall_min_level) + ' dB)'); -} - -function waterfallColorsDefault() { - waterfall_min_level = waterfall_min_level_default; - waterfall_max_level = waterfall_max_level_default; - updateWaterfallSliders(); - waterfallColorsContinuousReset(); -} - -function waterfallColorsAuto(levels) { - var min_level = levels.min - waterfall_auto_levels.min; - var max_level = levels.max + waterfall_auto_levels.max; - max_level = Math.max(min_level + (waterfall_auto_min_range || 0), max_level); - waterfall_min_level = min_level; - waterfall_max_level = max_level; - updateWaterfallSliders(); -} - -var waterfall_continuous = { - min: -150, - max: 0 -}; - -function waterfallColorsContinuousReset() { - waterfall_continuous.min = waterfall_min_level; - waterfall_continuous.max = waterfall_max_level; -} - -function waterfallColorsContinuous(levels) { - if (levels.max > waterfall_continuous.max + 1) { - waterfall_continuous.max += 1; - } else if (levels.max < waterfall_continuous.max - 1) { - waterfall_continuous.max -= .1; - } - if (levels.min < waterfall_continuous.min - 1) { - waterfall_continuous.min -= 1; - } else if (levels.min > waterfall_continuous.min + 1) { - waterfall_continuous.min += .1; - } - waterfallColorsAuto(waterfall_continuous); -} - function setSmeterRelativeValue(value) { if (value < 0) value = 0; if (value > 1.0) value = 1.0; @@ -231,10 +157,9 @@ function getLogSmeterValue(value) { function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` { var logValue = getLogSmeterValue(value); + var levels = Waterfall.getRange(); + var percent = (logValue - (levels.min - 20)) / ((levels.max + 20) - (levels.min - 20)); setSquelchSliderBackground(logValue); - var lowLevel = waterfall_min_level - 20; - var highLevel = waterfall_max_level + 20; - var percent = (logValue - lowLevel) / (highLevel - lowLevel); setSmeterRelativeValue(percent); $("#openwebrx-smeter-db").html(logValue.toFixed(1) + " dB"); } @@ -976,22 +901,12 @@ function on_ws_recv(evt) { switch (json.type) { case "config": var config = json['value']; + + // Configure waterfall min/max levels, etc + Waterfall.configure(config); + if ('waterfall_colors' in config) UI.setDefaultWfTheme(config['waterfall_colors']); - if ('waterfall_levels' in config) { - waterfall_min_level_default = config['waterfall_levels']['min']; - waterfall_max_level_default = config['waterfall_levels']['max']; - } - if ('waterfall_auto_levels' in config) - waterfall_auto_levels = config['waterfall_auto_levels']; - if ('waterfall_auto_min_range' in config) - waterfall_auto_min_range = config['waterfall_auto_min_range']; - if ('waterfall_auto_level_default_mode' in config) - waterfall_measure_minmax_continuous = config['waterfall_auto_level_default_mode']; - - var waterfallAutoButton = $('#openwebrx-waterfall-colors-auto'); - waterfallAutoButton[waterfall_measure_minmax_continuous ? 'addClass' : 'removeClass']('highlighted'); - $('#openwebrx-waterfall-color-min, #openwebrx-waterfall-color-max').prop('disabled', waterfall_measure_minmax_continuous); var initial_demodulator_params = {}; if ('start_mod' in config) @@ -1042,7 +957,7 @@ function on_ws_recv(evt) { } if ('sdr_id' in config || 'profile_id' in config || 'waterfall_levels' in config) { - waterfallColorsDefault(); + Waterfall.setDefaultRange(); } if ('tuning_precision' in config) @@ -1252,28 +1167,6 @@ function on_ws_recv(evt) { } } -function waterfall_measure_minmax_do(what) { - // Get visible range - var range = get_visible_freq_range(); - var start = center_freq - bandwidth / 2; - - // This is based on an oversampling factor of about 1,25 - range.start = Math.max(0.1, (range.start - start) / bandwidth); - range.end = Math.min(0.9, (range.end - start) / bandwidth); - - // Align to the range edges, do not let things flip over - if (range.start >= 0.9) - range.start = range.end - range.bw / bandwidth; - else if (range.end <= 0.1) - range.end = range.start + range.bw / bandwidth; - - var data = what.slice(range.start * what.length, range.end * what.length); - return { - min: Math.min.apply(Math, data), - max: Math.max.apply(Math, data) - }; -} - function on_ws_opened() { $('#openwebrx-error-overlay').hide(); ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); @@ -1389,26 +1282,6 @@ function open_websocket() { ws.onerror = on_ws_error; } -function waterfall_mkcolor(db_value, waterfall_colors_arg) { - waterfall_colors_arg = waterfall_colors_arg || waterfall_colors; - var value_percent = (db_value - waterfall_min_level) / (waterfall_max_level - waterfall_min_level); - value_percent = Math.max(0, Math.min(1, value_percent)); - - var scaled = value_percent * (waterfall_colors_arg.length - 1); - var index = Math.floor(scaled); - var remain = scaled - index; - if (remain === 0) return waterfall_colors_arg[index]; - return color_between(waterfall_colors_arg[index], waterfall_colors_arg[index + 1], remain);} - -function color_between(first, second, percent) { - return [ - first[0] + percent * (second[0] - first[0]), - first[1] + percent * (second[1] - first[1]), - first[2] + percent * (second[2] - first[2]) - ]; -} - - var canvas_context; var canvases = []; var canvas_default_height = 200; @@ -1478,30 +1351,17 @@ function waterfall_add(data) { if (!waterfall_setup_done) return; var w = fft_size; - if (waterfall_measure_minmax_now) { - var levels = waterfall_measure_minmax_do(data); - waterfall_measure_minmax_now = false; - waterfallColorsAuto(levels); - waterfallColorsContinuousReset(); - } - - if (waterfall_measure_minmax_continuous) { - var level = waterfall_measure_minmax_do(data); - waterfallColorsContinuous(level); - } + // measure waterfall min/max levels, if necessary + Waterfall.measureRange(data); // create new canvas if the current one is full (or there isn't one) if (canvas_actual_line <= 0) add_canvas(); - //Add line to waterfall image + // add line to waterfall image var oneline_image = canvas_context.createImageData(w, 1); - for (var x = 0; x < w; x++) { - var color = waterfall_mkcolor(data[x]); - for (i = 0; i < 3; i++) oneline_image.data[x * 4 + i] = color[i]; - oneline_image.data[x * 4 + 3] = 255; - } + Waterfall.drawLine(oneline_image.data, data); - //Draw image + // draw image canvas_context.putImageData(oneline_image, 0, --canvas_actual_line); shift_canvases(); } @@ -1593,15 +1453,10 @@ function initSliders() { $slider.trigger('change'); }); - var waterfallAutoButton = $('#openwebrx-waterfall-colors-auto'); - waterfallAutoButton.on('click', function() { - waterfall_measure_minmax_now=true; - }).on('contextmenu', function(){ - waterfall_measure_minmax_continuous = !waterfall_measure_minmax_continuous; - waterfallColorsContinuousReset(); - waterfallAutoButton[waterfall_measure_minmax_continuous ? 'addClass' : 'removeClass']('highlighted'); - $('#openwebrx-waterfall-color-min, #openwebrx-waterfall-color-max').prop('disabled', waterfall_measure_minmax_continuous); - + // Enable continuous waterfall color adjustment by pressing the + // right mouse button on AUTO + $('#openwebrx-waterfall-colors-auto').on('contextmenu', function() { + Waterfall.toggleContinuousRange(); return false; }); @@ -1867,20 +1722,14 @@ function secondary_demod_push_data(x) { function secondary_demod_waterfall_add(data) { var w = secondary_fft_size; - //Add line to waterfall image + // add line to waterfall image var oneline_image = secondary_demod_current_canvas_context.createImageData(w, 1); - for (var x = 0; x < w; x++) { - var color = waterfall_mkcolor(data[x] + secondary_demod_fft_offset_db); - for (var i = 0; i < 3; i++) oneline_image.data[x * 4 + i] = color[i]; - oneline_image.data[x * 4 + 3] = 255; - } + Waterfall.drawLine(oneline_image.data, data, secondary_demod_fft_offset_db); - //Draw image + // draw image secondary_demod_current_canvas_context.putImageData(oneline_image, 0, secondary_demod_current_canvas_actual_line--); - secondary_demod_canvases.map(function (x) { - x.openwebrx_top += 1; - }) - ; + secondary_demod_canvases.map(function (x) { x.openwebrx_top += 1; }); + secondary_demod_canvases_update_top(); if (secondary_demod_current_canvas_actual_line < 0) secondary_demod_swap_canvases(); } diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index abd4a038..45785245 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -137,6 +137,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController): "lib/Js8Threads.js", "lib/Modes.js", "lib/MetaPanel.js", + "lib/Waterfall.js", "lib/Spectrum.js", "lib/Scanner.js", "lib/Utils.js",