/* This file is part of OpenWebRX, an open-source SDR receiver software with a web UI. Copyright (c) 2013-2015 by Andras Retzler 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")!=-1; function arrayBufferToString(buf) { //http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers return String.fromCharCode.apply(null, new Uint8Array(buf)); } function getFirstChars(buf, num) { var u8buf=new Uint8Array(buf); var output=String(); num=Math.min(num,u8buf.length); for(i=0;i=parseInt(wfmax.value)) { if(!which) wfmin.value=(parseInt(wfmax.value)-1).toString(); else wfmax.value=(parseInt(wfmin.value)+1).toString(); } waterfall_min_level=parseInt(wfmin.value); waterfall_max_level=parseInt(wfmax.value); } function waterfallColorsDefault() { waterfall_min_level=waterfall_min_level_default; waterfall_max_level=waterfall_max_level_default; e("openwebrx-waterfall-color-min").value=waterfall_min_level.toString(); e("openwebrx-waterfall-color-max").value=waterfall_max_level.toString(); } function waterfallColorsAuto() { e("openwebrx-waterfall-color-min").value=(waterfall_measure_minmax_min-waterfall_auto_level_margin[0]).toString(); e("openwebrx-waterfall-color-max").value=(waterfall_measure_minmax_max+waterfall_auto_level_margin[1]).toString(); updateWaterfallColors(0); } function setSmeterRelativeValue(value) { if(value<0) value=0; if(value>1.0) value=1.0; var bar=e("openwebrx-smeter-bar"); var outer=e("openwebrx-smeter-outer"); bar.style.width=(outer.offsetWidth*value).toString()+"px"; bgRed="linear-gradient(to top, #ff5939 , #961700)"; bgGreen="linear-gradient(to top, #22ff2f , #008908)"; bgYellow="linear-gradient(to top, #fff720 , #a49f00)"; bar.style.background=(value>0.9)?bgRed:((value>0.7)?bgYellow:bgGreen); //bar.style.backgroundColor=(value>0.9)?"#ff5939":((value>0.7)?"#fff720":"#22ff2f"); } function getLogSmeterValue(value) { return 10*Math.log10(value); } function getLinearSmeterValue(db_value) { return Math.pow(10,db_value/10); } function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` { var logValue=getLogSmeterValue(value); var lowLevel=waterfall_min_level-20; var highLevel=waterfall_max_level+20; var percent=(logValue-lowLevel)/(highLevel-lowLevel); setSmeterRelativeValue(percent); e("openwebrx-smeter-db").innerHTML=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 ); } // ======================================================== // ================= ANIMATION ROUTINES ================= // ======================================================== function animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec) { //console.log(object.className); if(typeof to_exec=="undefined") to_exec=0; object.style[style_name]=from.toString()+unit; object.anim_i=0; n_of_iters=time_ms/(1000/fps); change=(to-from)/(n_of_iters); if(typeof object.anim_timer!="undefined") { window.clearInterval(object.anim_timer); } object.anim_timer=window.setInterval( function(){ if(object.anim_i++9||unit!="px") new_val=(to+accel*remain); else {if(Math.abs(remain)<2) new_val=to; else new_val=to+remain-(remain/Math.abs(remain));} object.style[style_name]=new_val.toString()+unit; } } else {object.style[style_name]=to.toString()+unit; window.clearInterval(object.anim_timer); delete object.anim_timer; } if(to_exec!=0) to_exec(); },1000/fps); } function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) { from=parseFloat(style_value(object,style_name)); animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec); } // ======================================================== // ================ DEMODULATOR ROUTINES ================ // ======================================================== demodulators=[] demodulator_color_index=0; demodulator_colors=["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] function demodulators_get_next_color() { if(demodulator_color_index>=demodulator_colors.length) demodulator_color_index=0; return(demodulator_colors[demodulator_color_index++]); } function demod_envelope_draw(range, from, to, color, line) { // ____ // Draws a standard filter envelope like this: _/ \_ // Parameters are given in offset frequency (Hz). // Envelope is drawn on the scale canvas. // A "drag range" object is returned, containing information about the draggable areas of the envelope // (beginning, ending and the line showing the offset frequency). if(typeof color == "undefined") color="#ffff00"; //yellow env_bounding_line_w=5; // env_att_w=5; // _______ ___env_h2 in px ___|_____ env_h1=17; // _/| \_ ___env_h1 in px _/ |_ \_ env_h2=5; // |||env_att_line_w |_env_lineplus env_lineplus=1; // ||env_bounding_line_w env_line_click_area=6; //range=get_visible_freq_range(); from_px=scale_px_from_freq(from,range); to_px=scale_px_from_freq(to,range); if(to_pxwindow.innerWidth)) // out of screen? { drag_ranges.beginning={x1:from_px, x2: from_px+env_bounding_line_w+env_att_w}; drag_ranges.ending={x1:to_px-env_bounding_line_w-env_att_w, x2: to_px}; drag_ranges.whole_envelope={x1:from_px, x2: to_px}; drag_ranges.envelope_on_screen=true; scale_ctx.beginPath(); scale_ctx.moveTo(from_px,env_h1); scale_ctx.lineTo(from_px+env_bounding_line_w, env_h1); scale_ctx.lineTo(from_px+env_bounding_line_w+env_att_w, env_h2); scale_ctx.lineTo(to_px-env_bounding_line_w-env_att_w, env_h2); scale_ctx.lineTo(to_px-env_bounding_line_w, env_h1); scale_ctx.lineTo(to_px, env_h1); scale_ctx.globalAlpha = 0.3; scale_ctx.fill(); scale_ctx.globalAlpha = 1; scale_ctx.stroke(); } if(typeof line != "undefined") // out of screen? { line_px=scale_px_from_freq(line,range); if(!(line_px<0||line_px>window.innerWidth)) { drag_ranges.line={x1:line_px-env_line_click_area/2, x2: line_px+env_line_click_area/2}; drag_ranges.line_on_screen=true; scale_ctx.moveTo(line_px,env_h1+env_lineplus); scale_ctx.lineTo(line_px,env_h2-env_lineplus); scale_ctx.stroke(); } } return drag_ranges; } function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). in_range=function(x,range) { return range.x1<=x&&range.x2>=x; } dr=demodulator.draggable_ranges; if(key_modifiers.shiftKey) { //Check first: shift + center drag emulates BFO knob if(drag_ranges.line_on_screen&&in_range(x,drag_ranges.line)) return dr.bfo; //Check second: shift + envelope drag emulates PBF knob if(drag_ranges.envelope_on_screen&&in_range(x,drag_ranges.whole_envelope)) return dr.pbs; } if(drag_ranges.envelope_on_screen) { // For low and high cut: if(in_range(x,drag_ranges.beginning)) return dr.beginning; if(in_range(x,drag_ranges.ending)) return dr.ending; // Last priority: having clicked anything else on the envelope, without holding the shift key if(in_range(x,drag_ranges.whole_envelope)) return dr.anything_else; } return dr.none; //User doesn't drag the envelope for this demodulator } //******* class demodulator ******* // this can be used as a base class for ANY demodulator demodulator=function(offset_frequency) { //console.log("this too"); this.offset_frequency=offset_frequency; this.has_audio_output=true; this.has_text_output=false; this.envelope={}; this.color=demodulators_get_next_color(); this.stop=function(){}; } //ranges on filter envelope that can be dragged: demodulator.draggable_ranges={none: 0, beginning:1 /*from*/, ending: 2 /*to*/, anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 } //to which parameter these correspond in demod_envelope_draw() //******* class demodulator_default_analog ******* // This can be used as a base for basic audio demodulators. // It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB demodulator_response_time=50; //in ms; if we don't limit the number of SETs sent to the server, audio will underrun (possibly output buffer is cleared on SETs in GNU Radio function demodulator_default_analog(offset_frequency,subtype) { //console.log("hopefully this happens"); //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain demodulator.call(this,offset_frequency); this.subtype=subtype; this.filter={ min_passband: 100, high_cut_limit: (audio_server_output_rate/2)-1, //audio_context.sampleRate/2, low_cut_limit: (-audio_server_output_rate/2)+1 //-audio_context.sampleRate/2 }; //Subtypes only define some filter parameters and the mod string sent to server, //so you may set these parameters in your custom child class. //Why? As of demodulation is done on the server, difference is mainly on the server side. this.server_mod=subtype; if(subtype=="lsb") { this.low_cut=-3000; this.high_cut=-300; this.server_mod="ssb"; } else if(subtype=="usb") { this.low_cut=300; this.high_cut=3000; this.server_mod="ssb"; } else if(subtype=="cw") { this.low_cut=700; this.high_cut=900; this.server_mod="ssb"; } else if(subtype=="nfm") { this.low_cut=-4000; this.high_cut=4000; } else if(subtype=="am") { this.low_cut=-4000; this.high_cut=4000; } this.wait_for_timer=false; this.set_after=false; this.set=function() { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. if(!this.wait_for_timer) { this.doset(false); this.set_after=false; this.wait_for_timer=true; timeout_this=this; //http://stackoverflow.com/a/2130411 window.setTimeout(function() { timeout_this.wait_for_timer=false; if(timeout_this.set_after) timeout_this.set(); },demodulator_response_time); } else { this.set_after=true; } } this.doset=function(first_time) { //this function sends demodulator parameters to the server params = { "low_cut": this.low_cut, "high_cut": this.high_cut, "offset_freq": this.offset_frequency }; if (first_time) params.mod = this.server_mod; ws.send(JSON.stringify({"type":"dspcontrol","params":params})); } this.doset(true); //we set parameters on object creation //******* envelope object ******* // for drawing the filter envelope above scale this.envelope.parent=this; this.envelope.draw=function(visible_range) { this.visible_range=visible_range; this.drag_ranges=demod_envelope_draw(range, center_freq+this.parent.offset_frequency+this.parent.low_cut, center_freq+this.parent.offset_frequency+this.parent.high_cut, this.color,center_freq+this.parent.offset_frequency); }; // event handlers this.envelope.drag_start=function(x, key_modifiers) { this.key_modifiers=key_modifiers; this.dragged_range=demod_envelope_where_clicked(x,this.drag_ranges, key_modifiers); //console.log("dragged_range: "+this.dragged_range.toString()); this.drag_origin={ x: x, low_cut: this.parent.low_cut, high_cut: this.parent.high_cut, offset_frequency: this.parent.offset_frequency }; return this.dragged_range!=demodulator.draggable_ranges.none; }; this.envelope.drag_move=function(x) { dr=demodulator.draggable_ranges; if(this.dragged_range==dr.none) return false; // we return if user is not dragging (us) at all freq_change=Math.round(this.visible_range.hps*(x-this.drag_origin.x)); /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) { //we don't let the passband be too small if(this.parent.low_cut+new_freq_change<=this.parent.high_cut-this.parent.filter.min_passband) this.freq_change=new_freq_change; else return; } var new_value;*/ //dragging the line in the middle of the filter envelope while holding Shift does emulate //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged //Filter passband moves in the opposite direction than dragged, hence the minus below. minus=(this.dragged_range==dr.bfo)?-1:1; //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset //frequency. if(this.dragged_range==dr.beginning||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) { //we don't let low_cut go beyond its limits if((new_value=this.drag_origin.low_cut+minus*freq_change)=this.parent.high_cut) return true; this.parent.low_cut=new_value; } if(this.dragged_range==dr.ending||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) { //we don't let high_cut go beyond its limits if((new_value=this.drag_origin.high_cut+minus*freq_change)>this.parent.filter.high_cut_limit) return true; //nor the filter passband be too small if(new_value-this.parent.low_cutbandwidth/2||new_value<-bandwidth/2) return true; //we don't allow tuning above Nyquist frequency :-) this.parent.offset_frequency=new_value; } //now do the actual modifications: mkenvelopes(this.visible_range); this.parent.set(); //will have to change this when changing to multi-demodulator mode: e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+this.parent.offset_frequency,1e6,4); return true; }; this.envelope.drag_end=function(x) { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. demodulator_buttons_update(); to_return=this.dragged_range!=demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset this.dragged_range=demodulator.draggable_ranges.none; return to_return; }; } demodulator_default_analog.prototype=new demodulator(); function mkenvelopes(visible_range) //called from mkscale { 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;ibandwidth/2||to_what<-bandwidth/2) return; demodulators[0].offset_frequency=Math.round(to_what); demodulators[0].set(); mkenvelopes(get_visible_freq_range()); } // ======================================================== // =================== SCALE ROUTINES =================== // ======================================================== var scale_ctx; var scale_canvas; function scale_setup() { e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(window.innerWidth/2),1e6,4); scale_canvas=e("openwebrx-scale-canvas"); 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); resize_scale(); } 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) { with(scale_canvas_drag_params) { mouse_down=true; drag=false; start_x=evt.pageX; key_modifiers.shiftKey=evt.shiftKey; key_modifiers.altKey=evt.altKey; 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(); return (visible_range.start+visible_range.bw*(x/canvas_container.clientWidth))-center_freq; } function scale_canvas_mousemove(evt) { var event_handled; 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 (var i=0;i=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; } out.smallbw=freq/out.ratio; return true; } for(i=scale_markers_levels.length-1;i>=0;i--) { 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; } 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(); 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"; spacing=get_scale_mark_spacing(range); //console.log(spacing); marker_hz=Math.ceil(range.start/spacing.smallbw)*spacing.smallbw; 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; for(;;) { var x=scale_px_from_freq(marker_hz,range); if(x>window.innerWidth) break; 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=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.ratiowindow.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; var 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/23) { out=out.substr(0,at)+","+out.substr(at); at+=4; decimals-=3; } return out; } canvas_drag=false; canvas_drag_min_delta=1; canvas_mouse_down=false; function canvas_mousedown(evt) { 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; //element=e("webrx-freq-show"); relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; /*realX=(relativeX-element.clientWidth/2); maxX=(canvases[0].clientWidth-element.clientWidth); if(realX>maxX) realX=maxX; if(realX<0) realX=0; element.style.left=realX.toString()+"px";*/ 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 deltaY=canvas_drag_last_y-evt.pageY; //zoom_center_where=zoom_center_where_calc(evt.pageX); var dpx=range.hps*deltaX; if( !(zoom_center_rel+dpx>(bandwidth/2-canvas_container.clientWidth*(1-zoom_center_where)*range.hps)) && !(zoom_center_rel+dpx<-bandwidth/2+canvas_container.clientWidth*zoom_center_where*range.hps) ) { zoom_center_rel+=dpx; } // -((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); resize_canvases(false); canvas_drag_last_x=evt.pageX; canvas_drag_last_y=evt.pageY; mkscale(); } } else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); } function canvas_container_mouseout(evt) { canvas_end_drag(); } //function body_mouseup() { canvas_end_drag(); console.log("body_mouseup"); } //function window_mouseout() { canvas_end_drag(); console.log("document_mouseout"); } function canvas_mouseup(evt) { if(!waterfall_setup_done) return; relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; if(!canvas_drag) { //ws.send("SET offset_freq="+canvas_get_freq_offset(relativeX).toString()); demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); } 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-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; return screenposX/canvas_container.clientWidth; } function canvas_mousewheel(evt) { if(!waterfall_setup_done) return; //var i=Math.abs(evt.wheelDelta); //var dir=(i/evt.wheelDelta)<0; //console.log(evt); var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; var dir=(evt.deltaY/Math.abs(evt.deltaY))>0; //console.log(dir); //i/=120; /*while (i--)*/ zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); evt.preventDefault(); //evt.returnValue = false; //disable scrollbar move } zoom_max_level_hps=33; //Hz/pixel zoom_levels_count=14; function get_zoom_coeff_from_hps(hps) { var shown_bw=(window.innerWidth*hps); return bandwidth/shown_bw; } zoom_levels=[1]; zoom_level=0; zoom_freq=0; zoom_offset_px=0; zoom_center_rel=0; zoom_center_where=0; smeter_level=0; function mkzoomlevels() { zoom_levels=[1]; maxc=get_zoom_coeff_from_hps(zoom_max_level_hps); if(maxc<1) return; // logarithmic interpolation zoom_ratio = Math.pow(maxc, 1/zoom_levels_count); for(i=1;i=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(); } 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+canvas_container.clientWidth/2); //zoom to screen center instead of demod envelope zoom_center_rel=demodulators[0].offset_frequency; zoom_center_where=0.5+(zoom_center_rel/bandwidth); //this is a kind of hack console.log(zoom_center_where, zoom_center_rel, -canvases[0].offsetLeft+canvas_container.clientWidth/2); resize_canvases(true); mkscale(); } function zoom_calc() { winsize=canvas_container.clientWidth; 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_pxPlease change your operating system default settings in order to fix this.",1); } if(audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) break; //okay, we're done i++; } audio_client_resampling_factor=i; console.log("audio_calculate_resampling() :: "+audio_client_resampling_factor.toString()+", "+audio_server_output_rate.toString()); } debug_ws_data_received=0; max_clients_num=0; 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 if (evt.data.substr(0, 16) == "CLIENT DE SERVER") { divlog("Server acknowledged WebSocket connection."); } else { try { json = JSON.parse(evt.data) switch (json.type) { case "config": config = json.value; window.waterfall_colors = config.waterfall_colors; window.waterfall_min_level_default = config.waterfall_min_level; window.waterfall_max_level_default = config.waterfall_max_level; window.waterfall_auto_level_margin = config.waterfall_auto_level_margin; waterfallColorsDefault(); window.starting_mod = config.start_mod window.starting_offset_frequency = config.start_offset_frequency; window.audio_buffering_fill_to = config.client_audio_buffer_size; bandwidth = config.samp_rate; center_freq = config.center_freq + config.lfo_offset; fft_size = config.fft_size; fft_fps = config.fft_fps; audio_compression = config.audio_compression; divlog( "Audio stream is "+ ((audio_compression=="adpcm")?"compressed":"uncompressed")+"." ) fft_compression = config.fft_compression; divlog( "FFT stream is "+ ((fft_compression=="adpcm")?"compressed":"uncompressed")+"." ) max_clients_num = config.max_clients; waterfall_init(); audio_preinit(); if (audio_allowed && !audio_initialized) audio_init(); waterfall_clear(); break; case "secondary_config": window.secondary_fft_size = json.value.secondary_fft_size; window.secondary_bw = json.value.secondary_bw; window.if_samp_rate = json.value.if_samp_rate; secondary_demod_init_canvases(); break; case "receiver_details": var r = json.value; e('webrx-rx-title').innerHTML = r.receiver_name; e('webrx-rx-desc').innerHTML = r.receiver_location + ' | Loc: ' + r.receiver_qra + ', ASL: ' + r.receiver_asl + ' m, [maps]'; e('webrx-rx-photo-title').innerHTML = r.photo_title; e('webrx-rx-photo-desc').innerHTML = r.photo_desc; break; case "smeter": setSmeterAbsoluteValue(json.value); break; case "cpuusage": var server_cpu_usage = json.value; progressbar_set(e("openwebrx-bar-server-cpu"),server_cpu_usage/100,"Server CPU [" + server_cpu_usage + "%]",server_cpu_usage>85); 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 type = new Uint8Array(evt.data, 0, 1)[0] data = evt.data.slice(1) switch (type) { case 1: // FFT data if (fft_compression=="none") { waterfall_add_queue(new Float32Array(data)); } else if (fft_compression == "adpcm") { fft_codec.reset(); var waterfall_i16=fft_codec.decode(new Uint8Array(data)); var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); for(var i=0;iaudio_buffering_fill_to)) audio_init() break; case 3: // secondary FFT if (fft_compression == "none") { secondary_demod_waterfall_add_queue(new Float32Array(data)); } else if (fft_compression == "adpcm") { fft_codec.reset(); var waterfall_i16=fft_codec.decode(new Uint8Array(data)); var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); for(var i=0;iMath.max(fft_fps/2,20)) //in case of emergency { console.log("waterfall queue length:", waterfall_queue.length); add_problem("fft overflow"); while(waterfall_queue.length) waterfall_add(waterfall_queue.shift()); } } function on_ws_opened() { ws.send("SERVER DE CLIENT openwebrx.js"); divlog("WebSocket opened to "+ws_url); } var was_error=0; function divlog(what, is_error) { is_error=!!is_error; was_error |= is_error; if(is_error) { what=""+what+""; if(e("openwebrx-panel-log").openwebrxHidden) toggle_panel("openwebrx-panel-log"); //show panel if any error is present } e("openwebrx-debugdiv").innerHTML+=what+"
"; //var wls=e("openwebrx-log-scroll"); //wls.scrollTop=wls.scrollHeight; //scroll to bottom $(".nano").nanoScroller(); $(".nano").nanoScroller({ scroll: 'bottom' }); } var audio_context; var audio_initialized=0; var volume = 1.0; var volumeBeforeMute = 100.0; var mute = false; var audio_received = Array(); var audio_buffer_index = 0; var audio_resampler; var audio_codec=new sdrjs.ImaAdpcm(); var audio_compression="unknown"; var audio_node; //var audio_received_sample_rate = 48000; var audio_input_buffer_size; // Optimalise these if audio lags or is choppy: var audio_buffer_size; var audio_buffer_maximal_length_sec=3; //actual number of samples are calculated from sample rate var audio_buffer_decrease_to_on_overrun_sec=2.2; var audio_flush_interval_ms=500; //the interval in which audio_flush() is called var audio_prepared_buffers = Array(); var audio_rebuffer; var audio_last_output_buffer; var audio_last_output_offset = 0; var audio_buffering = false; //var audio_buffering_fill_to=4; //on audio underrun we wait until this n*audio_buffer_size samples are present //tnx to the hint from HA3FLT, now we have about half the response time! (original value: 10) function gain_ff(gain_value,data) //great! solved clicking! will have to move to sdr.js { for(var i=0;iaudio_buffering_fill_to) { console.log("buffers now: "+audio_prepared_buffers.length.toString()); audio_buffering=false; } } function audio_prepare_without_resampler(data) { audio_rebuffer.push(sdrjs.ConvertI16_F(data)); console.log("prepare",data.length,audio_rebuffer.remaining()); while(audio_rebuffer.remaining()) { audio_prepared_buffers.push(audio_rebuffer.take()); audio_buffer_current_count_debug++; } if(audio_buffering && audio_prepared_buffers.length>audio_buffering_fill_to) audio_buffering=false; } function audio_prepare_old(data) { //console.log("audio_prepare :: "+data.length.toString()); //console.log("data.len = "+data.length.toString()); var dopush=function() { console.log(audio_last_output_buffer); audio_prepared_buffers.push(audio_last_output_buffer); audio_last_output_offset=0; audio_last_output_buffer=new Float32Array(audio_buffer_size); audio_buffer_current_count_debug++; }; var original_data_length=data.length; var f32data=new Float32Array(data.length); for(var i=0;iaudio_buffering_fill_to) audio_buffering=false; } if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does AudioBuffer.prototype.copyToChannel=function(input,channel) //input is Float32Array { var cd=this.getChannelData(channel); for(var i=0;iaudio_buffer_maximal_length_sec; var underrun=audio_prepared_buffers.length==0; var text="buffer"; if(overrun) { text="overrun"; console.log("audio overrun, "+(++audio_overrun_cnt).toString()); } if(underrun) { text="underrun"; console.log("audio underrun, "+(++audio_underrun_cnt).toString()); } if(overrun||underrun) { audio_buffer_progressbar_update_disabled=true; window.setTimeout(function(){audio_buffer_progressbar_update_disabled=false; audio_buffer_progressbar_update();},1000); } progressbar_set(e("openwebrx-bar-audio-buffer"),(underrun)?1:audio_buffer_value/1.5,"Audio "+text+" ["+(audio_buffer_value).toFixed(1)+" s]",overrun||underrun||audio_buffer_value<0.25); } function audio_flush() { flushed=false; we_have_more_than=function(sec){ return sec*audio_context.sampleRateread_remain) { for (i=audio_buffer_index; i"+read_remain.toString()+" obi="+obi.toString()+"\n"; audio_buffer_index+=read_remain; break; } else { for (i=audio_buffer_index; iaudio_buffer_maximal_length) { add_problem("audio overrun"); audio_received.splice(0,audio_received.length-audio_buffer_maximal_length); } else*/ audio_received.splice(0,1); //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; audio_buffer_index = 0; if(audio_received.length == 0 || read_remain == 0) return; int_buffer = audio_received[0]; } } //debug_str+="obi="+obi.toString(); //alert(debug_str); } function audio_flush_notused() { if (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate) { add_problem("audio overrun"); console.log("audio_flush() :: size: "+audio_buffer_current_size.toString()+" allowed: "+(audio_buffer_maximal_length_sec*audio_context.sampleRate).toString()); while (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate*0.5) { audio_buffer_current_size-=audio_received[0].length; audio_received.splice(0,1); } } } function webrx_set_param(what, value) { params = {}; params[what] = value; ws.send(JSON.stringify({"type":"dspcontrol","params":params})); } var starting_mute = false; function parsehash() { if(h=window.location.hash) { h.substring(1).split(",").forEach(function(x){ harr=x.split("="); //console.log(harr); if(harr[0]=="mute") toggleMute(); else if(harr[0]=="mod") starting_mod = harr[1]; else if(harr[0]=="sql") { e("openwebrx-panel-squelch").value=harr[1]; updateSquelch(); } else if(harr[0]=="freq") { console.log(parseInt(harr[1])); console.log(center_freq); starting_offset_frequency = parseInt(harr[1])-center_freq; } }); } } function audio_preinit() { try { window.AudioContext = window.AudioContext||window.webkitAudioContext; audio_context = new AudioContext(); } catch(e) { divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); return; } if(audio_context.sampleRate<44100*2) audio_buffer_size = 4096; else if(audio_context.sampleRate>=44100*2 && audio_context.sampleRate<44100*4) audio_buffer_size = 4096 * 2; else if(audio_context.sampleRate>44100*4) audio_buffer_size = 4096 * 4; audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); audio_last_output_buffer = new Float32Array(audio_buffer_size); //we send our setup packet parsehash(); audio_calculate_resampling(audio_context.sampleRate); audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); ws.send(JSON.stringify({"type":"dspcontrol","action":"start","params":{"output_rate":audio_server_output_rate}})); } function audio_init() { if(is_chrome) audio_context.resume() if(starting_mute) toggleMute(); if(audio_client_resampling_factor==0) return; //if failed to find a valid resampling factor... audio_debug_time_start=(new Date()).getTime(); audio_debug_time_last_start=audio_debug_time_start; //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js audio_initialized=1; // only tell on_ws_recv() not to call it again //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor createjsnode_function = (audio_context.createJavaScriptNode == undefined)?audio_context.createScriptProcessor.bind(audio_context):audio_context.createJavaScriptNode.bind(audio_context); audio_node = createjsnode_function(audio_buffer_size, 0, 1); audio_node.onaudioprocess = audio_onprocess; audio_node.connect(audio_context.destination); // --- Resampling --- //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); webrx_set_param("audio_rate",audio_context.sampleRate); //Don't try to resample //TODO remove this window.setInterval(audio_flush,audio_flush_interval_ms); divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString()+ " sps"); /*audio_source=audio_context.createBufferSource(); audio_buffer = audio_context.createBuffer(xhr.response, false); audio_source.buffer = buffer; audio_source.noteOn(0);*/ demodulator_analog_replace(starting_mod); if(starting_offset_frequency) { demodulators[0].offset_frequency = starting_offset_frequency; e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+starting_offset_frequency,1e6,4); demodulators[0].set(); mkscale(); } //hide log panel in a second (if user has not hidden it yet) window.setTimeout(function(){ if(typeof e("openwebrx-panel-log").openwebrxHidden == "undefined" && !was_error) { toggle_panel("openwebrx-panel-log"); //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) } },2000); } function on_ws_closed() { try { audio_node.disconnect(); } catch (dont_care) {} divlog("WebSocket has closed unexpectedly. Attempting to reconnect in 5 seconds...", 1); setTimeout(open_websocket, 5000); } function on_ws_error(event) { divlog("WebSocket error.",1); } String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string function open_websocket() { ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour 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) { if(typeof waterfall_colors_arg === 'undefined') waterfall_colors_arg = waterfall_colors; if(db_valuewaterfall_max_level) db_value=waterfall_max_level; full_scale=waterfall_max_level-waterfall_min_level; relative_value=db_value-waterfall_min_level; value_percent=relative_value/full_scale; percent_for_one_color=1/(waterfall_colors_arg.length-1); index=Math.floor(value_percent/percent_for_one_color); remain=(value_percent-percent_for_one_color*index)/percent_for_one_color; return color_between(waterfall_colors_arg[index+1],waterfall_colors_arg[index],remain); } function color_between(first, second, percent) { output=0; for(i=0;i<4;i++) { add = ((((first&(0xff<<(i*8)))>>>0)*percent) + (((second&(0xff<<(i*8)))>>>0)*(1-percent))) & (0xff<<(i*8)); output |= add>>>0; } return output>>>0; } var canvas_context; var canvases = []; var canvas_default_height = 200; var canvas_container; var canvas_phantom; 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-1; new_canvas.style.width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; new_canvas.style.left=zoom_offset_px.toString()+"px"; new_canvas.style.height=canvas_default_height.toString()+"px"; new_canvas.openwebrx_top=(-canvas_default_height+1); new_canvas.style.top=new_canvas.openwebrx_top.toString()+"px"; canvas_context = new_canvas.getContext("2d"); canvas_container.appendChild(new_canvas); new_canvas.addEventListener("mouseover", canvas_mouseover, false); new_canvas.addEventListener("mouseout", canvas_mouseout, false); new_canvas.addEventListener("mousemove", canvas_mousemove, false); new_canvas.addEventListener("mouseup", canvas_mouseup, false); new_canvas.addEventListener("mousedown", canvas_mousedown, false); new_canvas.addEventListener("wheel",canvas_mousewheel, false); canvases.push(new_canvas); } function init_canvas_container() { canvas_container=e("webrx-canvas-container"); mathbox_container=e("openwebrx-mathbox-container"); canvas_container.addEventListener("mouseout",canvas_container_mouseout, false); //window.addEventListener("mouseout",window_mouseout,false); //document.body.addEventListener("mouseup",body_mouseup,false); canvas_phantom=e("openwebrx-phantom-canvas"); canvas_phantom.addEventListener("mouseover", canvas_mouseover, false); canvas_phantom.addEventListener("mouseout", canvas_mouseout, false); canvas_phantom.addEventListener("mousemove", canvas_mousemove, false); canvas_phantom.addEventListener("mouseup", canvas_mouseup, false); canvas_phantom.addEventListener("mousedown", canvas_mousedown, false); canvas_phantom.addEventListener("wheel",canvas_mousewheel, false); canvas_phantom.style.width=canvas_container.clientWidth+"px"; add_canvas(); } canvas_maxshift=0; function shift_canvases() { canvases.forEach(function(p) { p.style.top=(p.openwebrx_top++).toString()+"px"; }); canvas_maxshift++; if(canvas_container.clientHeight>canvas_maxshift) { canvas_phantom.style.top=canvas_maxshift.toString()+"px"; canvas_phantom.style.height=(canvas_container.clientHeight-canvas_maxshift).toString()+"px"; canvas_phantom.style.display="block"; } else canvas_phantom.style.display="none"; //canvas_container.style.height=(((canvases.length-1)*canvas_default_height)+(canvas_default_height-canvas_actual_line)).toString()+"px"; //canvas_container.style.height="100%"; } function resize_canvases(zoom) { if(typeof zoom == "undefined") zoom=false; if(!zoom) mkzoomlevels(); zoom_calc(); new_width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; var zoom_value=zoom_offset_px.toString()+"px"; canvases.forEach(function(p) { p.style.width=new_width; p.style.left=zoom_value; }); canvas_phantom.style.width=new_width; canvas_phantom.style.left=zoom_value; } function waterfall_init() { init_canvas_container(); waterfall_timer = window.setInterval(()=>{waterfall_dequeue(); secondary_demod_waterfall_dequeue();},900/fft_fps); resize_waterfall_container(false); /* then */ resize_canvases(); scale_setup(); mkzoomlevels(); waterfall_setup_done=1; } var waterfall_dont_scale=0; var mathbox_shift = function() { if(mathbox_data_current_depth < mathbox_data_max_depth) mathbox_data_current_depth++; if(mathbox_data_index+1>=mathbox_data_max_depth) mathbox_data_index = 0; else mathbox_data_index++; mathbox_data_global_index++; } var mathbox_clear_data = function() { mathbox_data_index = 50; mathbox_data_current_depth = 0; } //var mathbox_get_data_line = function(x) //x counts from 0 to mathbox_data_current_depth //{ // return (mathbox_data_max_depth + mathbox_data_index - mathbox_data_current_depth + x - 1) % mathbox_data_max_depth; //} // //var mathbox_data_index_valid = function(x) //x counts from 0 to mathbox_data_current_depth //{ // return xmathbox_data_max_depth-mathbox_data_current_depth; } function waterfall_add(data) { if(!waterfall_setup_done) return; var w=fft_size; //waterfall_shift(); // ==== do scaling if required ==== /*if(waterfall_dont_scale) { scaled=data; for(i=scaled.length;i1) { scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); remain--; } else { j++; scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); remain=pixel_per_point-(1-remain); } } } else { //make line smaller (linear decimation, moving average) point_per_pixel=(to-from)/w; scaled=Array(); j=0; remain=point_per_pixel; last_pixel=0; for(i=from; i1) { last_pixel+=data[i]; remain--; } else { last_pixel+=data[i]*remain; scaled[j++]=last_pixel/point_per_pixel; last_pixel=data[i]*(1-remain); remain=point_per_pixel-(1-remain); //? } } } } //Add line to waterfall image base=(h-1)*w*4; for(x=0;x>>0)>>((3-i)*8))&0xff; }*/ if (mathbox_mode==MATHBOX_MODES.WATERFALL) { //Handle mathbox for(var i=0;i>>0)>>((3-i)*8))&0xff; } //Draw image canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); shift_canvases(); if(canvas_actual_line<0) add_canvas(); } } /* function waterfall_shift() { w=canvas.width; h=canvas.height; for(y=0; ytl.offsetLeft-20) what.style.opacity=what.style.opacity="0"; else wet.style.opacity=wed.style.opacity="1"; }); } var MATHBOX_MODES = { UNINITIALIZED: 0, NONE: 1, WATERFALL: 2, CONSTELLATION: 3 }; var mathbox_mode = MATHBOX_MODES.UNINITIALIZED; var mathbox; var mathbox_element; function mathbox_init() { //mathbox_waterfall_history_length is defined in the config mathbox_data_max_depth = fft_fps * mathbox_waterfall_history_length; //how many lines can the buffer store mathbox_data_current_depth = 0; //how many lines are in the buffer currently mathbox_data_index = 0; //the index of the last empty line / the line to be overwritten mathbox_data = new Float32Array(fft_size * mathbox_data_max_depth); mathbox_data_global_index = 0; mathbox_correction_for_z = 0; mathbox = mathBox({ plugins: ['core', 'controls', 'cursor', 'stats'], controls: { klass: THREE.OrbitControls }, }); three = mathbox.three; if(typeof three == "undefined") divlog("3D waterfall cannot be initialized because WebGL is not supported in your browser.", true); three.renderer.setClearColor(new THREE.Color(0x808080), 1.0); mathbox_container.appendChild((mathbox_element=three.renderer.domElement)); view = mathbox .set({ scale: 1080, focus: 3, }) .camera({ proxy: true, position: [-2, 1, 3], }) .cartesian({ range: [[-1, 1], [0, 1], [0, 1]], scale: [2, 2/3, 1], }); view.axis({ axis: 1, width: 3, color: "#fff", }); view.axis({ axis: 2, width: 3, color: "#fff", //offset: [0, 0, 0], }); view.axis({ axis: 3, width: 3, color: "#fff", }); view.grid({ width: 2, opacity: 0.5, axes: [1, 3], zOrder: 1, color: "#fff", }); //var remap = function (v) { return Math.sqrt(.5 + .5 * v); }; var remap = function(x,z,t) { var currentTimePos = mathbox_data_global_index/(fft_fps*1.0); var realZAdd = (-(t-currentTimePos)/mathbox_waterfall_history_length); var zAdd = realZAdd - mathbox_correction_for_z; if(zAdd<-0.2 || zAdd>0.2) { mathbox_correction_for_z = realZAdd; } var xIndex = Math.trunc(((x+1)/2.0)*fft_size); //x: frequency var zIndex = Math.trunc(z*(mathbox_data_max_depth-1)); //z: time var realZIndex = mathbox_get_data_line(zIndex); if(!mathbox_data_index_valid(zIndex)) return {y: undefined, dBValue: undefined, zAdd: 0 }; //if(realZIndex>=(mathbox_data_max_depth-1)) console.log("realZIndexundef", realZIndex, zIndex); var index = Math.trunc(xIndex + realZIndex * fft_size); /*if(mathbox_data[index]==undefined) console.log("Undef", index, mathbox_data.length, zIndex, realZIndex, mathbox_data_max_depth, mathbox_data_current_depth, mathbox_data_index);*/ var dBValue = mathbox_data[index]; //y=1; if(dBValue>waterfall_max_level) y = 1; else if(dBValue>8)/255.0; var r = ((color&0xff0000)>>16)/255.0; emit(r, g, b, 1.0); }, width: mathbox_waterfall_frequency_resolution, height: mathbox_data_max_depth - 1, channels: 4, axes: [1, 3], }); view.surface({ shaded: true, points: '<<', colors: '<', color: 0xFFFFFF, }); view.surface({ fill: false, lineX: false, lineY: false, points: '<<', colors: '<', color: 0xFFFFFF, width: 2, blending: 'add', opacity: .25, zBias: 5, }); mathbox_mode = MATHBOX_MODES.NONE; //mathbox_element.style.width="100%"; //mathbox_element.style.height="100%"; } function mathbox_toggle() { if(mathbox_mode == MATHBOX_MODES.UNINITIALIZED) mathbox_init(); mathbox_mode = (mathbox_mode == MATHBOX_MODES.NONE) ? MATHBOX_MODES.WATERFALL : MATHBOX_MODES.NONE; mathbox_container.style.display = (mathbox_mode == MATHBOX_MODES.WATERFALL) ? "block" : "none"; mathbox_clear_data(); waterfall_clear(); } function waterfall_clear() { while(canvases.length) //delete all canvases { var x=canvases.shift(); x.parentNode.removeChild(x); delete x; } add_canvas(); } function openwebrx_resize() { resize_canvases(); resize_waterfall_container(true); resize_scale(); check_top_bar_congestion(); } function openwebrx_init() { if(ios||is_chrome) e("openwebrx-big-grey").style.display="table-cell"; (opb=e("openwebrx-play-button-text")).style.marginTop=(window.innerHeight/2-opb.clientHeight/2).toString()+"px"; init_rx_photo(); open_websocket(); secondary_demod_init(); place_panels(first_show_panel); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.addEventListener("resize",openwebrx_resize); check_top_bar_congestion(); //Synchronise volume with slider updateVolume(); } function iosPlayButtonClick() { //On iOS, we can only start audio from a click or touch event. audio_init(); e("openwebrx-big-grey").style.opacity=0; window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100); audio_allowed = 1; } /* window.setInterval(function(){ sum=0; for(i=0;i=(c=c.charCodeAt(0)+13)?c:c-26);}); window.location.href="mailto:"+what; }*/ 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);});} var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});} var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); } var audio_debug_time_start=0; var audio_debug_time_last_start=0; function debug_audio() { if(audio_debug_time_start==0) return; //audio_init has not been called time_now=(new Date()).getTime(); audio_debug_time_since_last_call=(time_now-audio_debug_time_last_start)/1000; audio_debug_time_last_start=time_now; //now audio_debug_time_taken=(time_now-audio_debug_time_start)/1000; kbps_mult=(audio_compression=="adpcm")?8:16; //e("openwebrx-audio-sps").innerHTML= // ((audio_compression=="adpcm")?"ADPCM compressed":"uncompressed")+" audio downlink:
"+(audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call).toFixed(0)+" kbps ("+ // (audio_buffer_all_size_debug*kbps_mult/audio_debug_time_taken).toFixed(1)+" kbps avg.), feed at "+ // ((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output"; var audio_speed_value=audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call; progressbar_set(e("openwebrx-bar-audio-speed"),audio_speed_value/500000,"Audio stream ["+(audio_speed_value/1000).toFixed(0)+" kbps]",false); var audio_output_value=(audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken; progressbar_set(e("openwebrx-bar-audio-output"),audio_output_value/55000,"Audio output ["+(audio_output_value/1000).toFixed(1)+" ksps]",audio_output_value>55000||audio_output_value<10000); audio_buffer_progressbar_update(); var network_speed_value=debug_ws_data_received/audio_debug_time_taken; progressbar_set(e("openwebrx-bar-network-speed"),network_speed_value*8/2000,"Network usage ["+(network_speed_value*8).toFixed(1)+" kbps]",false); audio_buffer_current_size_debug=0; if(waterfall_measure_minmax) waterfall_measure_minmax_print(); } // ======================================================== // ======================= PANELS ======================= // ======================================================== panel_margin=5.9; function pop_bottommost_panel(from) { min_order=parseInt(from[0].dataset.panelOrder); min_index=0; for(i=0;i0.5)?-90:90; roty=0; if(Math.random()>0.5) { rottemp=rotx; rotx=roty; roty=rottemp; } if(rotx!=0 && Math.random()>0.5) rotx=270; //console.log(rotx,roty); transformString = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" .replace("%1",rotx.toString()).replace("%2",roty.toString()); //console.log(transformString); //console.log(panel); panel.style.transform=transformString; window.setTimeout(function() { panel.style.transitionDuration="599ms"; panel.style.transitionDelay=(Math.floor(Math.random()*500)).toString()+"ms"; panel.style.transform="perspective( 599px ) rotateX( 0deg ) rotateY( 0deg )"; //panel.style.transitionDuration="0ms"; //panel.style.transitionDelay="0"; }, 1); } function place_panels(function_apply) { if(function_apply == undefined) function_apply = function(x){}; var hoffset=0; //added this because the first panel should not have such great gap below var left_col=[]; var right_col=[]; var plist=e("openwebrx-panels-container").children; for(i=0;i0) { p=pop_bottommost_panel(left_col); p.style.left="0px"; p.style.bottom=y.toString()+"px"; p.style.visibility="visible"; y+=p.openwebrxPanelHeight+((p.openwebrxPanelTransparent)?0:3)*panel_margin; if(function_apply) function_apply(p); //console.log(p.id, y, p.openwebrxPanelTransparent); } y=hoffset; while(right_col.length>0) { p=pop_bottommost_panel(right_col); p.style.right=(e("webrx-canvas-container").offsetWidth-e("webrx-canvas-container").clientWidth).toString()+"px"; //get scrollbar width p.style.bottom=y.toString()+"px"; p.style.visibility="visible"; y+=p.openwebrxPanelHeight+((p.openwebrxPanelTransparent)?0:3)*panel_margin; if(function_apply) function_apply(p); } } function progressbar_set(obj,val,text,over) { if (val<0.05) val=0; if (val>1) val=1; var innerBar=null; var innerText=null; for(var i=0;i0) $("#openwebrx-button-usb").addClass("highlighted"); else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); } break; } } function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); } /* _____ _ _ _ | __ \(_) (_) | | | | | |_ __ _ _ _ __ ___ ___ __| | ___ ___ | | | | |/ _` | | '_ ` _ \ / _ \ / _` |/ _ \/ __| | |__| | | (_| | | | | | | | (_) | (_| | __/\__ \ |_____/|_|\__, |_|_| |_| |_|\___/ \__,_|\___||___/ __/ | |___/ */ secondary_demod = false; secondary_demod_offset_freq = 0; secondary_demod_waterfall_queue = []; function demodulator_digital_replace_last() { demodulator_digital_replace(last_digital_demodulator_subtype); secondary_demod_listbox_update(); } function demodulator_digital_replace(subtype) { switch(subtype) { case "bpsk31": case "rtty": secondary_demod_start(subtype); demodulator_analog_replace('usb', true); demodulator_buttons_update(); break; } toggle_panel("openwebrx-panel-digimodes", true); } 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"; console.log(new_canvas.width, new_canvas.height, new_canvas.style.width, new_canvas.style.height); 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; //secondary_demod_update_channel_freq_from_event(); 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.top=secondary_demod_canvases[i].openwebrx_top+"px"; } function secondary_demod_swap_canvases() { console.log("swap"); 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() { $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; 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_mouseout); } function secondary_demod_start(subtype) { secondary_demod_canvases_initialized = false; ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":subtype}})); secondary_demod = subtype; } function secondary_demod_set() { ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":secondary_demod_offset_freq}})); } function secondary_demod_stop() { ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_mod":false}})); secondary_demod = false; secondary_demod_waterfall_queue = []; } function secondary_demod_waterfall_add_queue(x) { secondary_demod_waterfall_queue.push(x); } function secondary_demod_push_binary_data(x) { secondary_demod_push_data(Array.from(x).map( y => (y)?"1":"0" ).join("")); } function secondary_demod_push_data(x) { x=Array.from(x).map((y)=>{ var c=y.charCodeAt(0); if(y=="\r") return " "; if(y=="\n") return " "; //if(y=="\n") return "
"; if(c<32||c>126) return ""; if(y=="&") return "&"; if(y=="<") return "<"; if(y==">") return ">"; if(y==" ") return " "; return y; }).join(""); $("#openwebrx-cursor-blink").before(""+x+""); } function secondary_demod_data_clear() { $("#openwebrx-cursor-blink").prevAll().remove(); } function secondary_demod_close_window() { secondary_demod_stop(); toggle_panel("openwebrx-panel-digimodes", false); } secondary_demod_fft_offset_db=30; //need to calculate that later function secondary_demod_waterfall_add(data) { if(!secondary_demod) return; var w=secondary_fft_size; //Add line to waterfall image var oneline_image = secondary_demod_current_canvas_context.createImageData(w,1); for(x=0;x>>0)>>((3-i)*8))&0xff; } //Draw image secondary_demod_current_canvas_context.putImageData(oneline_image, 0, secondary_demod_current_canvas_actual_line--); secondary_demod_canvases.map((x)=>{x.openwebrx_top += 1;}); secondary_demod_canvases_update_top(); if(secondary_demod_current_canvas_actual_line<0) secondary_demod_swap_canvases(); } var secondary_demod_canvases_initialized = false; function secondary_demod_waterfall_dequeue() { if(!secondary_demod || !secondary_demod_canvases_initialized) return; if(secondary_demod_waterfall_queue.length) secondary_demod_waterfall_add(secondary_demod_waterfall_queue.shift()); if(secondary_demod_waterfall_queue.length>Math.max(fft_fps/2,20)) //in case of fft overflow { console.log("secondary waterfall overflow, queue length:", secondary_demod_waterfall_queue.length); while(secondary_demod_waterfall_queue.length) secondary_demod_waterfall_add(secondary_demod_waterfall_queue.shift()); } } secondary_demod_listbox_updating = false; function secondary_demod_listbox_changed() { if(secondary_demod_listbox_updating) return; switch ($("#openwebrx-secondary-demod-listbox")[0].value) { case "none": demodulator_analog_replace_last(); break; case "bpsk31": demodulator_digital_replace('bpsk31'); break; case "rtty": demodulator_digital_replace('rtty'); break; } } function secondary_demod_listbox_update() { secondary_demod_listbox_updating = true; $("#openwebrx-secondary-demod-listbox").val((secondary_demod)?secondary_demod:"none"); console.log("update"); secondary_demod_listbox_updating = false; } secondary_demod_channel_freq=1000; function secondary_demod_update_marker() { var width = Math.max( (secondary_bw / (if_samp_rate/2)) * secondary_demod_canvas_width, 5); var center_at = (secondary_demod_channel_freq / (if_samp_rate/2)) * secondary_demod_canvas_width + secondary_demod_canvas_left; var left = center_at-width/2; //console.log("sdum", width, left); $("#openwebrx-digimode-select-channel").width(width).css("left",left+"px") } secondary_demod_waiting_for_set = false; 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); } //console.log("toset:", secondary_demod_channel_freq); if(!secondary_demod_waiting_for_set) { secondary_demod_waiting_for_set = true; window.setTimeout(()=>{ ws.send(JSON.stringify({"type":"dspcontrol","params":{"secondary_offset_freq":Math.floor(secondary_demod_channel_freq)}})); //console.log("doneset:", secondary_demod_channel_freq); secondary_demod_waiting_for_set = false; }, 50); } secondary_demod_update_marker(); } secondary_demod_mousedown=false; function secondary_demod_canvas_container_mousein() { $("#openwebrx-digimode-select-channel").css("opacity","0.7"); //.css("border-width", "1px"); } function secondary_demod_canvas_container_mouseout() { $("#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 || !secondary_demod_canvases_initialized) return; if(low_cut<0 && high_cut<0) { var hctmp = high_cut; var lctmp = low_cut; low_cut = -hctmp; low_cut = -lctmp; } else if(low_cut<0 && high_cut>0) { high_cut=Math.max(Math.abs(high_cut), Math.abs(low_cut)); low_cut=0; } 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/2)/shown_bw; secondary_demod_canvas_left = -secondary_demod_canvas_width*(low_cut/(if_samp_rate/2)); //console.log("setzoom", secondary_demod_canvas_width, secondary_demod_canvas_left, low_cut, high_cut); secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");}); secondary_demod_update_channel_freq_from_event(); }