/* This file is part of OpenWebRX, an open-source SDR receiver software with a web UI. Copyright (c) 2013-2015 by Andras Retzler Copyright (c) 2019-2021 by Jakob Ketterl This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ */ is_firefox = navigator.userAgent.indexOf("Firefox") >= 0; var bandwidth; var center_freq; var fft_size; var fft_compression = "none"; var fft_codec; var waterfall_setup_done = 0; var secondary_fft_size; var tuning_step_default = 1; var tuning_step = 1; var nr_enabled = false; var nr_threshold = 0; function updateVolume() { audioEngine.setVolume(parseFloat($("#openwebrx-panel-volume").val()) / 100); } function toggleMute() { var $muteButton = $('.openwebrx-mute-button'); var $volumePanel = $('#openwebrx-panel-volume'); if ($muteButton.hasClass('muted')) { $muteButton.removeClass('muted'); $volumePanel.prop('disabled', false).val(volumeBeforeMute); } else { $muteButton.addClass('muted'); volumeBeforeMute = $volumePanel.val(); $volumePanel.prop('disabled', true).val(0); } updateVolume(); } function updateNR() { var $nrPanel = $('#openwebrx-panel-nr'); nr_threshold = Math.round(parseFloat($nrPanel.val())); $nrPanel.attr('title', 'Noise level (' + nr_threshold + ' dB)'); nr_changed(); } function toggleNR() { var $nrPanel = $('#openwebrx-panel-nr'); if ($nrPanel.prop('disabled')) { $nrPanel.prop('disabled', false); nr_enabled = true; } else { $nrPanel.prop('disabled', true); nr_enabled = false; } nr_changed(); } function zoomInOneStep() { zoom_set(zoom_level + 1); } function zoomOutOneStep() { zoom_set(zoom_level - 1); } function zoomInTotal() { zoom_set(zoom_levels.length - 1); } function zoomOutTotal() { zoom_set(0); } var waterfall_min_level; var waterfall_max_level; var waterfall_min_level_default; var waterfall_max_level_default; var waterfall_colors = buildWaterfallColors(['#000', '#FFF']); var waterfall_auto_levels; var waterfall_auto_min_range; function buildWaterfallColors(input) { return chroma.scale(input).colors(256, 'rgb') } 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; var $meter = $("#openwebrx-smeter"); var $bar = $meter.find(".openwebrx-smeter-bar"); $bar.css({transform: 'translate(' + ((value - 1) * 100) + '%) translateZ(0)'}); if (value > 0.9) { // red $bar.css({background: 'linear-gradient(to top, #ff5939 , #961700)'}); } else if (value > 0.7) { // yellow $bar.css({background: 'linear-gradient(to top, #fff720 , #a49f00)'}); } else { // red $bar.css({background: 'linear-gradient(to top, #22ff2f , #008908)'}); } } function setSquelchSliderBackground(val) { var $slider = $('#openwebrx-panel-receiver .openwebrx-squelch-slider'); var min = Number($slider.attr('min')); var max = Number($slider.attr('max')); var sliderPosition = $slider.val(); var relative = (val - min) / (max - min); // use a brighter color when squelch is open var color = val >= sliderPosition ? '#22ff2f' : '#008908'; // we don't use the gradient, but separate the colors discretely using css tricks var style = 'linear-gradient(90deg, ' + color + ', ' + color + ' ' + relative * 100 + '%, #B6B6B6 ' + relative * 100 + '%)'; $slider.css('--track-background', style); } function getLogSmeterValue(value) { return 10 * Math.log10(value); } function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` { var logValue = getLogSmeterValue(value); 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"); } function typeInAnimation(element, timeout, what, onFinish) { if (!what) { onFinish(); return; } element.innerHTML += what[0]; window.setTimeout(function () { typeInAnimation(element, timeout, what.substring(1), onFinish); }, timeout); } // ======================================================== // ================ DEMODULATOR ROUTINES ================ // ======================================================== function getDemodulators() { return [ $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator() ].filter(function(d) { return !!d; }); } function mkenvelopes(visible_range) //called from mkscale { var demodulators = getDemodulators(); scale_ctx.clearRect(0, 0, scale_ctx.canvas.width, 22); //clear the upper part of the canvas (where filter envelopes reside) for (var i = 0; i < demodulators.length; i++) { demodulators[i].envelope.draw(visible_range); } if (demodulators.length) { var bandpass = demodulators[0].getBandpass(); secondary_demod_waterfall_set_zoom(bandpass.low_cut, bandpass.high_cut); } } function waterfallWidth() { return $('body').width(); } // ======================================================== // =================== SCALE ROUTINES =================== // ======================================================== var scale_ctx; var scale_canvas; function scale_setup() { scale_canvas = $("#openwebrx-scale-canvas")[0]; scale_ctx = scale_canvas.getContext("2d"); scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); scale_canvas.addEventListener("touchmove", process_touch, false); scale_canvas.addEventListener("touchend", process_touch, false); scale_canvas.addEventListener("touchstart", process_touch, false); resize_scale(); var frequency_container = $("#openwebrx-frequency-container"); frequency_container.on("mousemove", frequency_container_mousemove, false); } var scale_canvas_drag_params = { mouse_down: false, drag: false, start_x: 0, key_modifiers: {shiftKey: false, altKey: false, ctrlKey: false} }; function scale_canvas_mousedown(evt) { // Left button only if (evt.button == 0) { scale_canvas_drag_params.mouse_down = true; scale_canvas_drag_params.drag = false; scale_canvas_drag_params.start_x = evt.pageX; scale_canvas_drag_params.key_modifiers.shiftKey = evt.shiftKey; scale_canvas_drag_params.key_modifiers.altKey = evt.altKey; scale_canvas_drag_params.key_modifiers.ctrlKey = evt.ctrlKey; } evt.preventDefault(); } function scale_offset_freq_from_px(x, visible_range) { if (typeof visible_range === "undefined") visible_range = get_visible_freq_range(); var f = (visible_range.start + visible_range.bw * (x / waterfallWidth())) - center_freq; return tuning_step>0? Math.round(f / tuning_step) * tuning_step : f; } function scale_canvas_mousemove(evt) { var event_handled = false; var i; var demodulators = getDemodulators(); if (scale_canvas_drag_params.mouse_down && !scale_canvas_drag_params.drag && Math.abs(evt.pageX - scale_canvas_drag_params.start_x) > canvas_drag_min_delta) //we can use the main drag_min_delta thing of the main canvas { scale_canvas_drag_params.drag = true; //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) for (i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_start(evt.pageX, scale_canvas_drag_params.key_modifiers); scale_canvas.style.cursor = "move"; } else if (scale_canvas_drag_params.drag) { //call the drag_move for all demodulators (and they will decide if they're dragged) for (i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_move(evt.pageX); if (!event_handled) demodulators[0].set_offset_frequency(scale_offset_freq_from_px(evt.pageX)); } } function frequency_container_mousemove(evt) { var frequency = center_freq + scale_offset_freq_from_px(evt.pageX); $('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(frequency); } function scale_canvas_end_drag(x) { scale_canvas.style.cursor = "default"; scale_canvas_drag_params.drag = false; scale_canvas_drag_params.mouse_down = false; var event_handled = false; var demodulators = getDemodulators(); for (var i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_end(); if (!event_handled) demodulators[0].set_offset_frequency(scale_offset_freq_from_px(x)); } function scale_canvas_mouseup(evt) { if (evt.button == 0) scale_canvas_end_drag(evt.pageX); } function scale_px_from_freq(f, range) { return Math.round(((f - range.start) / range.bw) * waterfallWidth()); } function get_visible_freq_range() { if (!bandwidth) return false; var fcalc = function (x) { var canvasWidth = waterfallWidth() * zoom_levels[zoom_level]; return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2); }; var out = { start: fcalc(0), center: fcalc(waterfallWidth() / 2), end: fcalc(waterfallWidth()), } out.bw = out.end - out.start; out.hps = out.bw / waterfallWidth(); return out; } var scale_markers_levels = [ { "large_marker_per_hz": 10000000, //large "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 0 }, { "large_marker_per_hz": 5000000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 0 }, { "large_marker_per_hz": 1000000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 0 }, { "large_marker_per_hz": 500000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 1 }, { "large_marker_per_hz": 100000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 1 }, { "large_marker_per_hz": 50000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 2 }, { "large_marker_per_hz": 10000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 2 }, { "large_marker_per_hz": 5000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 3 }, { "large_marker_per_hz": 1000, "estimated_text_width": 70, "format": "{x} MHz", "pre_divide": 1000000, "decimals": 1 } ]; var scale_min_space_bw_texts = 50; var scale_min_space_bw_small_markers = 7; function get_scale_mark_spacing(range) { var out = {}; var fcalc = function (freq) { out.numlarge = (range.bw / freq); out.large = waterfallWidth() / out.numlarge; //distance between large markers (these have text) out.ratio = 5; //(ratio-1) small markers exist per large marker out.small = out.large / out.ratio; //distance between small markers if (out.small < scale_min_space_bw_small_markers) return false; if (out.small / 2 >= scale_min_space_bw_small_markers && freq.toString()[0] !== "5") { out.small /= 2; out.ratio *= 2; } out.smallbw = freq / out.ratio; return true; }; for (var i = scale_markers_levels.length - 1; i >= 0; i--) { var mp = scale_markers_levels[i]; if (!fcalc(mp.large_marker_per_hz)) continue; //console.log(mp.large_marker_per_hz); //console.log(out); if (out.large - mp.estimated_text_width > scale_min_space_bw_texts) break; } out.params = mp; return out; } var range; function mkscale() { //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): range = get_visible_freq_range(); if (!range) return; mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too scale_ctx.clearRect(0, 22, scale_ctx.canvas.width, scale_ctx.canvas.height - 22); scale_ctx.strokeStyle = "#fff"; scale_ctx.font = "bold 11px sans-serif"; scale_ctx.textBaseline = "top"; scale_ctx.fillStyle = "#fff"; var spacing = get_scale_mark_spacing(range); //console.log(spacing); var marker_hz = Math.ceil(range.start / spacing.smallbw) * spacing.smallbw; var text_h_pos = 22 + 10 + ((is_firefox) ? 3 : 0); var text_to_draw = ''; var ftext = function (f) { text_to_draw = format_frequency(spacing.params.format, f, spacing.params.pre_divide, spacing.params.decimals); }; var last_large; var x; while ((x = scale_px_from_freq(marker_hz, range)) <= window.innerWidth) { scale_ctx.beginPath(); scale_ctx.moveTo(x, 22); if (marker_hz % spacing.params.large_marker_per_hz === 0) { //large marker if (typeof first_large === "undefined") var first_large = marker_hz; last_large = marker_hz; scale_ctx.lineWidth = 3.5; scale_ctx.lineTo(x, 22 + 11); ftext(marker_hz); var text_measured = scale_ctx.measureText(text_to_draw); scale_ctx.textAlign = "center"; //advanced text drawing begins if (zoom_level === 0 && (range.start + spacing.smallbw * spacing.ratio > marker_hz) && (x < text_measured.width / 2)) { //if this is the first overall marker when zoomed out... and if it would be clipped off the screen... if (scale_px_from_freq(marker_hz + spacing.smallbw * spacing.ratio, range) - text_measured.width >= scale_min_space_bw_texts) { //and if we have enough space to draw it correctly without clipping scale_ctx.textAlign = "left"; scale_ctx.fillText(text_to_draw, 0, text_h_pos); } } else if (zoom_level === 0 && (range.end - spacing.smallbw * spacing.ratio < marker_hz) && (x > window.innerWidth - text_measured.width / 2)) { // if this is the last overall marker when zoomed out... and if it would be clipped off the screen... if (window.innerWidth - text_measured.width - scale_px_from_freq(marker_hz - spacing.smallbw * spacing.ratio, range) >= scale_min_space_bw_texts) { //and if we have enough space to draw it correctly without clipping scale_ctx.textAlign = "right"; scale_ctx.fillText(text_to_draw, window.innerWidth, text_h_pos); } } else scale_ctx.fillText(text_to_draw, x, text_h_pos); //draw text normally } else { //small marker scale_ctx.lineWidth = 2; scale_ctx.lineTo(x, 22 + 8); } marker_hz += spacing.smallbw; scale_ctx.stroke(); } if (zoom_level !== 0) { // if zoomed, we don't want the texts to disappear because their markers can't be seen // on the left side scale_ctx.textAlign = "center"; var f = first_large - spacing.smallbw * spacing.ratio; x = scale_px_from_freq(f, range); ftext(f); var w = scale_ctx.measureText(text_to_draw).width; if (x + w / 2 > 0) scale_ctx.fillText(text_to_draw, x, 22 + 10); // on the right side f = last_large + spacing.smallbw * spacing.ratio; x = scale_px_from_freq(f, range); ftext(f); w = scale_ctx.measureText(text_to_draw).width; if (x - w / 2 < window.innerWidth) scale_ctx.fillText(text_to_draw, x, 22 + 10); } } function resize_scale() { var ratio = window.devicePixelRatio || 1; var w = window.innerWidth; var h = 47; scale_canvas.style.width = w + "px"; scale_canvas.style.height = h + "px"; w *= ratio; h *= ratio; scale_canvas.width = w; scale_canvas.height = h; scale_ctx.scale(ratio, ratio); mkscale(); bookmarks.position(); } function canvas_get_freq_offset(relativeX) { var rel = (relativeX / canvas_container.clientWidth); var off = (bandwidth * rel) - (bandwidth / 2); return tuning_step>0? Math.round(off / tuning_step) * tuning_step : Math.round(off); } function canvas_get_frequency(relativeX) { var f = center_freq + canvas_get_freq_offset(relativeX); return tuning_step>0? Math.round(f / tuning_step) * tuning_step : f; } function format_frequency(format, freq_hz, pre_divide, decimals) { var out = format.replace("{x}", (freq_hz / pre_divide).toFixed(decimals)); var at = out.indexOf(".") + 4; while (decimals > 3) { out = out.substr(0, at) + "," + out.substr(at); at += 4; decimals -= 3; } return out; } var canvas_drag = false; var canvas_drag_min_delta = 1; var canvas_mouse_down = false; var canvas_mouse2_down = 0; var canvas_drag_last_x; var canvas_drag_last_y; var canvas_drag_start_x; var canvas_drag_start_y; function process_touch(evt) { var t0 = evt.changedTouches[0]; var mouseEvt = document.createEvent("MouseEvent"); var type = ""; switch(evt.type) { case "touchstart": type = "mousedown"; break; case "touchmove": type = "mousemove"; break; case "touchend": type = "mouseup"; break; default: return; } mouseEvt.initMouseEvent(type, true, true, window, 1, t0.screenX, t0.screenY, t0.clientX, t0.clientY, false, false, false, false, 0/*left*/, null); t0.target.dispatchEvent(mouseEvt); evt.preventDefault(); } function canvas_mousedown(evt) { if (evt.button > 0) { if (canvas_mouse2_down == 0) canvas_mouse2_down = evt.button; } else { canvas_mouse_down = true; canvas_drag = false; canvas_drag_last_x = canvas_drag_start_x = evt.pageX; canvas_drag_last_y = canvas_drag_start_y = evt.pageY; } evt.preventDefault(); //don't show text selection mouse pointer } function canvas_mousemove(evt) { if (!waterfall_setup_done) return; var relativeX = get_relative_x(evt); if (canvas_mouse_down) { if (!canvas_drag && Math.abs(evt.pageX - canvas_drag_start_x) > canvas_drag_min_delta) { canvas_drag = true; canvas_container.style.cursor = "move"; } if (canvas_drag) { var deltaX = canvas_drag_last_x - evt.pageX; var dpx = range.hps * deltaX; if ( !(zoom_center_rel + dpx > (bandwidth / 2 - waterfallWidth() * (1 - zoom_center_where) * range.hps)) && !(zoom_center_rel + dpx < -bandwidth / 2 + waterfallWidth() * zoom_center_where * range.hps) ) { zoom_center_rel += dpx; } resize_canvases(false); canvas_drag_last_x = evt.pageX; canvas_drag_last_y = evt.pageY; mkscale(); bookmarks.position(); } } else { $('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(canvas_get_frequency(relativeX)); } } function canvas_container_mouseleave() { canvas_end_drag(); } function canvas_mouseup(evt) { if (evt.button > 0) { if (evt.button == canvas_mouse2_down) canvas_mouse2_down = 0; } else { if (!waterfall_setup_done) return; var relativeX = get_relative_x(evt); if (!canvas_drag) { var demodulator = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator(); var f = canvas_get_freq_offset(relativeX); // For CW, move offset 800Hz below the actual carrier if (demodulator.get_modulation() === 'cw') { f = f - 800; } demodulator.set_offset_frequency(f); } else { canvas_end_drag(); } canvas_mouse_down = false; } } function canvas_end_drag() { canvas_container.style.cursor = "crosshair"; canvas_mouse_down = false; } function zoom_center_where_calc(screenposX) { return screenposX / waterfallWidth(); } function get_relative_x(evt) { var relativeX = evt.offsetX || evt.layerX; if ($(evt.target).closest(canvas_container).length) return relativeX; // compensate for the frequency scale, since that is not resized by the browser. var relatives = $(evt.target).closest('#openwebrx-frequency-container').map(function(){ return evt.pageX - this.offsetLeft; }); if (relatives.length) relativeX = relatives[0]; return relativeX - zoom_offset_px; } function canvas_mousewheel(evt) { if (!waterfall_setup_done) return; var relativeX = get_relative_x(evt); var dir = (evt.deltaY / Math.abs(evt.deltaY)) > 0; // Zoom when mouse button down, tune otherwise if (canvas_mouse2_down > 0) { zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); } else { var f = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency(); f += dir? -tuning_step : tuning_step; $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().set_offset_frequency(f); } evt.preventDefault(); } var zoom_max_level_hps = 33; //Hz/pixel var zoom_levels_count = 14; function get_zoom_coeff_from_hps(hps) { var shown_bw = (window.innerWidth * hps); return bandwidth / shown_bw; } var zoom_levels = [1]; var zoom_level = 0; var zoom_offset_px = 0; var zoom_center_rel = 0; var zoom_center_where = 0; var smeter_level = 0; function mkzoomlevels() { zoom_levels = [1]; 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)); } 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; 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); mkscale(); bookmarks.position(); } function zoom_set(level) { if (!(level >= 0 && level <= zoom_levels.length - 1)) return; level = parseInt(level); zoom_level = 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); mkscale(); bookmarks.position(); } function zoom_calc() { var winsize = waterfallWidth(); var canvases_new_width = winsize * zoom_levels[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) zoom_offset_px = winsize - canvases_new_width; } var networkSpeedMeasurement; var currentprofile = { toString: function() { return this['sdr_id'] + '|' + this['profile_id']; } }; var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c function on_ws_recv(evt) { if (typeof evt.data === 'string') { // text messages networkSpeedMeasurement.add(evt.data.length); if (evt.data.substr(0, 16) === "CLIENT DE SERVER") { params = Object.fromEntries( evt.data.slice(17).split(' ').map(function(param) { var args = param.split('='); return [args[0], args.slice(1).join('=')] }) ); var versionInfo = 'Unknown server'; if (params.server && params.server === 'openwebrx' && params.version) { versionInfo = 'OpenWebRX version: ' + params.version; } divlog('Server acknowledged WebSocket connection, ' + versionInfo); } else { try { var json = JSON.parse(evt.data); switch (json.type) { case "config": var config = json['value']; if ('waterfall_colors' in config) waterfall_colors = buildWaterfallColors(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']; waterfallColorsDefault(); var initial_demodulator_params = {}; if ('start_mod' in config) initial_demodulator_params['mod'] = config['start_mod']; if ('start_offset_freq' in config) initial_demodulator_params['offset_frequency'] = config['start_offset_freq']; if ('initial_squelch_level' in config) initial_demodulator_params['squelch_level'] = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150; if ('samp_rate' in config) bandwidth = config['samp_rate']; if ('center_freq' in config) center_freq = config['center_freq']; if ('fft_size' in config) { fft_size = config['fft_size']; waterfall_clear(); } if ('audio_compression' in config) { var audio_compression = config['audio_compression']; audioEngine.setCompression(audio_compression); divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); } if ('fft_compression' in config) { fft_compression = config['fft_compression']; divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); } if ('max_clients' in config) $('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']); waterfall_init(); var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel(); demodulatorPanel.setCenterFrequency(center_freq); demodulatorPanel.setInitialParams(initial_demodulator_params); if ('squelch_auto_margin' in config) demodulatorPanel.setSquelchMargin(config['squelch_auto_margin']); bookmarks.loadLocalBookmarks(); if ('sdr_id' in config || 'profile_id' in config) { currentprofile['sdr_id'] = config['sdr_id'] || currentprofile['sdr_id']; currentprofile['profile_id'] = config['profile_id'] || currentprofile['profile_id']; $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString()); waterfall_clear(); zoom_set(0); } if ('tuning_precision' in config) $('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']); if ('tuning_step' in config) { tuning_step_default = config['tuning_step']; tuning_step_reset(); } break; case "secondary_config": var s = json['value']; secondary_fft_size = s['secondary_fft_size'] || secondary_fft_size; secondary_bw = s['secondary_bw'] || secondary_bw; if_samp_rate = s['if_samp_rate'] || if_samp_rate; if (if_samp_rate) secondary_demod_init_canvases(); break; case "receiver_details": $('.webrx-top-container').header().setDetails(json['value']); break; case "smeter": smeter_level = json['value']; setSmeterAbsoluteValue(smeter_level); break; case "cpuusage": $('#openwebrx-bar-server-cpu').progressbar().setUsage(json['value']); break; case "clients": $('#openwebrx-bar-clients').progressbar().setClients(json['value']); break; case "profiles": var listbox = $("#openwebrx-sdr-profiles-listbox"); listbox.html(json['value'].map(function (profile) { return '"; }).join("")); $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString()); // this is a bit hacky since it only makes sense if the error is actually "no sdr devices" // the only other error condition for which the overlay is used right now is "too many users" // so there shouldn't be a problem here if (Object.keys(json['value']).length) { $('#openwebrx-error-overlay').hide(); } break; case "features": Modes.setFeatures(json['value']); break; case "metadata": $('.openwebrx-meta-panel').metaPanel().each(function(){ this.update(json['value']); }); break; case "dial_frequencies": var as_bookmarks = json['value'].map(function (d) { return { name: d['mode'].toUpperCase(), modulation: d['mode'], frequency: d['frequency'] }; }); bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); break; case "bookmarks": bookmarks.replace_bookmarks(json['value'], "server"); break; case "sdr_error": divlog(json['value'], true); var $overlay = $('#openwebrx-error-overlay'); $overlay.find('.errormessage').text(json['value']); $overlay.show(); $("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator(); break; case "demodulator_error": divlog(json['value'], true); break; case 'secondary_demod': var value = json['value']; var panels = [ $("#openwebrx-panel-wsjt-message").wsjtMessagePanel(), $('#openwebrx-panel-packet-message').packetMessagePanel(), $('#openwebrx-panel-pocsag-message').pocsagMessagePanel(), $("#openwebrx-panel-js8-message").js8() ]; if (!panels.some(function(panel) { if (!panel.supportsMessage(value)) return false; panel.pushMessage(value); return true; })) { secondary_demod_push_data(value); } break; case 'log_message': divlog(json['value'], true); break; case 'backoff': divlog("Server is currently busy: " + json['reason'], true); var $overlay = $('#openwebrx-error-overlay'); $overlay.find('.errormessage').text(json['reason']); $overlay.show(); // set a higher reconnection timeout right away to avoid additional load reconnect_timeout = 16000; break; case 'modes': Modes.setModes(json['value']); break; default: console.warn('received message of unknown type: ' + json['type']); } } catch (e) { // don't lose exception console.error(e) } } } else if (evt.data instanceof ArrayBuffer) { // binary messages networkSpeedMeasurement.add(evt.data.byteLength); var type = new Uint8Array(evt.data, 0, 1)[0]; var data = evt.data.slice(1); var waterfall_i16; var waterfall_f32; var i; switch (type) { case 1: // FFT data if (fft_compression === "none") { waterfall_add(new Float32Array(data)); } else if (fft_compression === "adpcm") { fft_codec.reset(); waterfall_i16 = fft_codec.decode(new Uint8Array(data)); waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); for (i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; waterfall_add(waterfall_f32); } break; case 2: // audio data audioEngine.pushAudio(data); break; case 3: // secondary FFT if (fft_compression === "none") { secondary_demod_waterfall_add(new Float32Array(data)); } else if (fft_compression === "adpcm") { fft_codec.reset(); waterfall_i16 = fft_codec.decode(new Uint8Array(data)); waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); for (i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; secondary_demod_waterfall_add(waterfall_f32); } break; case 4: // hd audio data audioEngine.pushHdAudio(data); break; default: console.warn('unknown type of binary message: ' + type) } } } var waterfall_measure_minmax_now = false; var waterfall_measure_minmax_continuous = false; 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); 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"); divlog("WebSocket opened to " + ws.url); if (!networkSpeedMeasurement) { networkSpeedMeasurement = new Measurement(); networkSpeedMeasurement.report(60000, 1000, function(rate){ $('#openwebrx-bar-network-speed').progressbar().setSpeed(rate); }); } else { networkSpeedMeasurement.reset(); } reconnect_timeout = false; ws.send(JSON.stringify({ "type": "connectionproperties", "params": { "output_rate": audioEngine.getOutputRate(), "hd_output_rate": audioEngine.getHdOutputRate() } })); } var was_error = 0; function divlog(what, is_error) { is_error = !!is_error; was_error |= is_error; if (is_error) { what = "" + what + ""; toggle_panel("openwebrx-panel-log", true); //show panel if any error is present } $('#openwebrx-debugdiv')[0].innerHTML += what + "
"; var nano = $('.nano'); nano.nanoScroller(); nano.nanoScroller({scroll: 'bottom'}); } var volumeBeforeMute = 100.0; var mute = false; // Optimalise these if audio lags or is choppy: var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate function onAudioStart(apiType){ divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz"); hideOverlay(); // canvas_container is set after waterfall_init() has been called. we cannot initialize before. //if (canvas_container) synchronize_demodulator_init(); //hide log panel in a second (if user has not hidden it yet) window.setTimeout(function () { toggle_panel("openwebrx-panel-log", !!was_error); }, 2000); //Synchronise volume with slider updateVolume(); } var reconnect_timeout = false; function on_ws_closed() { var demodulatorPanel = $("#openwebrx-panel-receiver").demodulatorPanel(); demodulatorPanel.stopDemodulator(); demodulatorPanel.resetInitialParams(); if (reconnect_timeout) { // max value: roundabout 8 and a half minutes reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); } else { // initial value: 1s reconnect_timeout = 1000; } divlog("WebSocket has closed unexpectedly. Attempting to reconnect in " + reconnect_timeout / 1000 + " seconds...", 1); setTimeout(open_websocket, reconnect_timeout); } function on_ws_error() { divlog("WebSocket error.", 1); } var ws; function open_websocket() { var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws'; var href = window.location.href; var index = href.lastIndexOf('/'); if (index > 0) { href = href.substr(0, index + 1); } href = href.split("://")[1]; href = protocol + "://" + href; if (!href.endsWith('/')) { href += '/'; } var ws_url = href + "ws/"; if (!("WebSocket" in window)) divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); ws = new WebSocket(ws_url); ws.onopen = on_ws_opened; ws.onmessage = on_ws_recv; ws.onclose = on_ws_closed; ws.binaryType = "arraybuffer"; window.onbeforeunload = function () { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript ws.onclose = function () { }; ws.close(); }; 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; var canvas_container; var canvas_actual_line = -1; function add_canvas() { var new_canvas = document.createElement("canvas"); new_canvas.width = fft_size; new_canvas.height = canvas_default_height; canvas_actual_line = canvas_default_height; new_canvas.openwebrx_top = -canvas_default_height; new_canvas.style.transform = 'translate(0, ' + new_canvas.openwebrx_top.toString() + 'px)'; canvas_context = new_canvas.getContext("2d"); canvas_container.appendChild(new_canvas); canvases.push(new_canvas); while (canvas_container && canvas_container.clientHeight + canvas_default_height * 2 < canvases.length * canvas_default_height) { var c = canvases.shift(); if (!c) break; canvas_container.removeChild(c); } } function init_canvas_container() { canvas_container = $("#webrx-canvas-container")[0]; canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false); canvas_container.addEventListener("mousemove", canvas_mousemove, false); canvas_container.addEventListener("mouseup", canvas_mouseup, false); canvas_container.addEventListener("mousedown", canvas_mousedown, false); canvas_container.addEventListener("wheel", canvas_mousewheel, false); canvas_container.addEventListener("touchmove", process_touch, false); canvas_container.addEventListener("touchend", process_touch, false); canvas_container.addEventListener("touchstart", process_touch, false); var frequency_container = $("#openwebrx-frequency-container"); frequency_container.on("wheel", canvas_mousewheel, false); } canvas_maxshift = 0; function shift_canvases() { canvases.forEach(function (p) { p.style.transform = 'translate(0, ' + (p.openwebrx_top++).toString() + 'px)'; }); canvas_maxshift++; } function resize_canvases(zoom) { if (typeof zoom === "undefined") zoom = false; if (!zoom) mkzoomlevels(); zoom_calc(); $('#webrx-canvas-container').css({ width: waterfallWidth() * zoom_levels[zoom_level] + 'px', left: zoom_offset_px + "px" }); } function waterfall_init() { init_canvas_container(); resize_canvases(); scale_setup(); mkzoomlevels(); waterfall_setup_done = 1; } 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); } // 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 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; } //Draw image canvas_context.putImageData(oneline_image, 0, --canvas_actual_line); shift_canvases(); } function waterfall_clear() { //delete all canvases while (canvases.length) { var x = canvases.shift(); x.parentNode.removeChild(x); } canvas_actual_line = -1; } function openwebrx_resize() { resize_canvases(); resize_scale(); } function initProgressBars() { $(".openwebrx-progressbar").each(function(){ var bar = $(this).progressbar(); if ('setSampleRate' in bar) { bar.setSampleRate(audioEngine.getSampleRate()); } }) } function audioReporter(stats) { if (typeof(stats.buffersize) !== 'undefined') { $('#openwebrx-bar-audio-buffer').progressbar().setBuffersize(stats.buffersize); } if (typeof(stats.audioByteRate) !== 'undefined') { $('#openwebrx-bar-audio-speed').progressbar().setSpeed(stats.audioByteRate * 8); } if (typeof(stats.audioRate) !== 'undefined') { $('#openwebrx-bar-audio-output').progressbar().setAudioRate(stats.audioRate); } } var bookmarks; var audioEngine; function openwebrx_init() { audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter); var $overlay = $('#openwebrx-autoplay-overlay'); $overlay.on('click', function(){ audioEngine.resume(); }); audioEngine.onStart(onAudioStart); if (!audioEngine.isAllowed()) { $('body').append($overlay); $overlay.show(); } fft_codec = new ImaAdpcmCodec(); initProgressBars(); open_websocket(); secondary_demod_init(); digimodes_init(); initPanels(); $('#openwebrx-panel-receiver').demodulatorPanel(); window.addEventListener("resize", openwebrx_resize); bookmarks = new BookmarkBar(); initSliders(); } function initSliders() { $('#openwebrx-panel-receiver').on('wheel', 'input[type=range]', function(ev){ var $slider = $(this); if (!$slider.attr('step')) return; var val = Number($slider.val()); var step = Number($slider.attr('step')); if (ev.originalEvent.deltaY > 0) { step *= -1; } $slider.val(val + step); $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); return false; }); } function digimodes_init() { // initialze DMR timeslot muting $('.openwebrx-dmr-timeslot-panel').click(function (e) { $(e.currentTarget).toggleClass("muted"); update_dmr_timeslot_filtering(); // don't mute when the location icon is clicked }).find('.location').click(function(e) { e.stopPropagation(); }); $('.openwebrx-meta-panel').metaPanel(); } function update_dmr_timeslot_filtering() { var filter = $('.openwebrx-dmr-timeslot-panel').map(function (index, el) { return (!$(el).hasClass("muted")) << index; }).toArray().reduce(function (acc, v) { return acc | v; }, 0); $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDmrFilter(filter); } function hideOverlay() { var $overlay = $('#openwebrx-autoplay-overlay'); $overlay.css('opacity', 0); $overlay.on('transitionend', function() { $overlay.hide(); }); } var rt = function (s, n) { return s.replace(/[a-zA-Z]/g, function (c) { return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + n) ? c : c - 26); }); }; // ======================================================== // ======================= PANELS ======================= // ======================================================== function panel_displayed(el){ return !(el.style && el.style.display && el.style.display === 'none') && !(el.movement && el.movement === 'collapse'); } function toggle_panel(what, on) { var item = $('#' + what)[0]; if (!item) return; var displayed = panel_displayed(item); if (typeof on !== "undefined" && displayed === on) { return; } if (displayed) { item.movement = 'collapse'; item.style.transform = "perspective(600px) rotateX(90deg)"; item.style.transitionProperty = 'transform'; } else { item.movement = 'expand'; item.style.display = null; setTimeout(function(){ item.style.transitionProperty = 'transform'; item.style.transform = 'perspective(600px) rotateX(0deg)'; }, 20); } item.style.transitionDuration = "600ms"; item.style.transitionDelay = "0ms"; } function first_show_panel(panel) { panel.style.transitionDuration = 0; panel.style.transitionDelay = 0; var rotx = (Math.random() > 0.5) ? -90 : 90; var roty = 0; if (Math.random() > 0.5) { var rottemp = rotx; rotx = roty; roty = rottemp; } if (rotx !== 0 && Math.random() > 0.5) rotx = 270; panel.style.transform = "perspective(600px) rotateX(%1deg) rotateY(%2deg)" .replace("%1", rotx.toString()).replace("%2", roty.toString()); window.setTimeout(function () { panel.style.transitionDuration = "600ms"; panel.style.transitionDelay = (Math.floor(Math.random() * 500)).toString() + "ms"; panel.style.transform = "perspective(600px) rotateX(0deg) rotateY(0deg)"; }, 1); } function initPanels() { $('#openwebrx-panels-container').find('.openwebrx-panel').each(function(){ var el = this; el.openwebrxPanelTransparent = (!!el.dataset.panelTransparent); el.addEventListener('transitionend', function(ev){ if (ev.target !== el) return; el.style.transitionDuration = null; el.style.transitionDelay = null; el.style.transitionProperty = null; if (el.movement && el.movement === 'collapse') { el.style.display = 'none'; } delete el.movement; }); if (panel_displayed(el)) first_show_panel(el); }); } /* _____ _ _ _ | __ \(_) (_) | | | | | |_ __ _ _ _ __ ___ ___ __| | ___ ___ | | | | |/ _` | | '_ ` _ \ / _ \ / _` |/ _ \/ __| | |__| | | (_| | | | | | | | (_) | (_| | __/\__ \ |_____/|_|\__, |_|_| |_| |_|\___/ \__,_|\___||___/ __/ | |___/ */ var secondary_demod_fft_offset_db = 18; //need to calculate that later var secondary_demod_canvases_initialized = false; var secondary_demod_channel_freq = 1000; var secondary_demod_waiting_for_set = false; var secondary_demod_low_cut; var secondary_demod_high_cut; var secondary_demod_mousedown = false; var secondary_demod_canvas_width; var secondary_demod_canvas_left; var secondary_demod_canvas_container; var secondary_demod_current_canvas_actual_line; var secondary_demod_current_canvas_context; var secondary_demod_current_canvas_index; var secondary_demod_canvases; var secondary_bw = 31.25; var if_samp_rate; function secondary_demod_create_canvas() { var new_canvas = document.createElement("canvas"); new_canvas.width = secondary_fft_size; new_canvas.height = $(secondary_demod_canvas_container).height(); new_canvas.style.width = $(secondary_demod_canvas_container).width() + "px"; new_canvas.style.height = $(secondary_demod_canvas_container).height() + "px"; secondary_demod_current_canvas_actual_line = new_canvas.height - 1; $(secondary_demod_canvas_container).children().last().before(new_canvas); return new_canvas; } function secondary_demod_remove_canvases() { $(secondary_demod_canvas_container).children("canvas").remove(); } function secondary_demod_init_canvases() { secondary_demod_remove_canvases(); secondary_demod_canvases = []; secondary_demod_canvases.push(secondary_demod_create_canvas()); secondary_demod_canvases.push(secondary_demod_create_canvas()); secondary_demod_canvases[0].openwebrx_top = -$(secondary_demod_canvas_container).height(); secondary_demod_canvases[1].openwebrx_top = 0; secondary_demod_canvases_update_top(); secondary_demod_current_canvas_context = secondary_demod_canvases[0].getContext("2d"); secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; secondary_demod_current_canvas_index = 0; secondary_demod_canvases_initialized = true; mkscale(); //so that the secondary waterfall zoom level will be initialized } function secondary_demod_canvases_update_top() { for (var i = 0; i < 2; i++) { secondary_demod_canvases[i].style.transform = 'translate(0, ' + secondary_demod_canvases[i].openwebrx_top + 'px)'; } } function secondary_demod_swap_canvases() { secondary_demod_canvases[0 + !secondary_demod_current_canvas_index].openwebrx_top -= $(secondary_demod_canvas_container).height() * 2; secondary_demod_current_canvas_index = 0 + !secondary_demod_current_canvas_index; secondary_demod_current_canvas_context = secondary_demod_canvases[secondary_demod_current_canvas_index].getContext("2d"); secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; } function secondary_demod_init() { secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; $(secondary_demod_canvas_container) .mousemove(secondary_demod_canvas_container_mousemove) .mouseup(secondary_demod_canvas_container_mouseup) .mousedown(secondary_demod_canvas_container_mousedown) .mouseenter(secondary_demod_canvas_container_mousein) .mouseleave(secondary_demod_canvas_container_mouseleave); $('#openwebrx-panel-wsjt-message').wsjtMessagePanel(); $('#openwebrx-panel-packet-message').packetMessagePanel(); $('#openwebrx-panel-pocsag-message').pocsagMessagePanel(); $('#openwebrx-panel-js8-message').js8(); } function secondary_demod_push_data(x) { x = Array.from(x).filter(function (y) { var c = y.charCodeAt(0); return (c === 10 || (c >= 32 && c <= 126)); }).map(function (y) { if (y === "&") return "&"; if (y === "<") return "<"; if (y === ">") return ">"; if (y === " ") return " "; if (y === "\n") return "
"; return y; }).join(""); $("#openwebrx-cursor-blink").before(x); } function secondary_demod_waterfall_add(data) { var w = secondary_fft_size; //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; } //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_update_top(); if (secondary_demod_current_canvas_actual_line < 0) secondary_demod_swap_canvases(); } function secondary_demod_update_marker() { var width = Math.max((secondary_bw / if_samp_rate) * secondary_demod_canvas_width, 5); var center_at = ((secondary_demod_channel_freq - secondary_demod_low_cut) / if_samp_rate) * secondary_demod_canvas_width; var left = center_at - width / 2; $("#openwebrx-digimode-select-channel").width(width).css("left", left + "px") } function secondary_demod_update_channel_freq_from_event(evt) { if (typeof evt !== "undefined") { var relativeX = (evt.offsetX) ? evt.offsetX : evt.layerX; secondary_demod_channel_freq = secondary_demod_low_cut + (relativeX / $(secondary_demod_canvas_container).width()) * (secondary_demod_high_cut - secondary_demod_low_cut); } if (!secondary_demod_waiting_for_set) { secondary_demod_waiting_for_set = true; window.setTimeout(function () { $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().set_secondary_offset_freq(Math.floor(secondary_demod_channel_freq)); secondary_demod_waiting_for_set = false; }, 50 ) ; } secondary_demod_update_marker(); } function secondary_demod_canvas_container_mousein() { $("#openwebrx-digimode-select-channel").css("opacity", "0.7"); //.css("border-width", "1px"); } function secondary_demod_canvas_container_mouseleave() { $("#openwebrx-digimode-select-channel").css("opacity", "0"); } function secondary_demod_canvas_container_mousemove(evt) { if (secondary_demod_mousedown) secondary_demod_update_channel_freq_from_event(evt); } function secondary_demod_canvas_container_mousedown(evt) { if (evt.which === 1) secondary_demod_mousedown = true; } function secondary_demod_canvas_container_mouseup(evt) { if (evt.which === 1) secondary_demod_mousedown = false; secondary_demod_update_channel_freq_from_event(evt); } function secondary_demod_waterfall_set_zoom(low_cut, high_cut) { if (!secondary_demod_canvases_initialized) return; secondary_demod_low_cut = low_cut; secondary_demod_high_cut = high_cut; var shown_bw = high_cut - low_cut; secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate) / shown_bw; secondary_demod_canvas_left = (-secondary_demod_canvas_width / 2) - (low_cut / if_samp_rate) * secondary_demod_canvas_width; secondary_demod_canvases.map(function (x) { $(x).css({ left: secondary_demod_canvas_left + "px", width: secondary_demod_canvas_width + "px" }); }); secondary_demod_update_channel_freq_from_event(); } function sdr_profile_changed() { var value = $('#openwebrx-sdr-profiles-listbox').val(); ws.send(JSON.stringify({type: "selectprofile", params: {profile: value}})); } function tuning_step_changed() { tuning_step = parseInt($('#openwebrx-tuning-step-listbox').val()); } function tuning_step_reset() { $('#openwebrx-tuning-step-listbox').val(tuning_step_default); tuning_step = tuning_step_default; } function nr_changed() { ws.send(JSON.stringify({ "type": "connectionproperties", "params": { "nr_enabled": nr_enabled, "nr_threshold": nr_threshold } })); }