/* 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-20).toString(); e("openwebrx-waterfall-color-max").value=(waterfall_measure_minmax_max+30).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 ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+ " low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+ " offset_freq="+this.offset_frequency.toString()); } 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. 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=5; 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; 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(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; } // debug_ws_data_received+=evt.data.byteLength/1000; firstChars=getFirstChars(evt.data,3); if(firstChars=="CLI") { var stringData=arrayBufferToString(evt.data); if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Acknowledged WebSocket connection: "+stringData); } if(firstChars=="AUD") { var audio_data; if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4) else audio_data=new Int16Array(evt.data,4); audio_prepare(audio_data); audio_buffer_current_size_debug+=audio_data.length; audio_buffer_all_size_debug+=audio_data.length; if(!ios && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init() } else if(firstChars=="FFT") { //alert("Yupee! Doing FFT"); if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4)); else if(fft_compression="adpcm") { fft_codec.reset(); var waterfall_i16=fft_codec.decode(new Uint8Array(evt.data,4)); var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); for(var i=0;i85); break; case "clients": var clients_num=parseInt(param[1]); progressbar_set(e("openwebrx-bar-clients"),clients_num/max_clients_num,"Clients ["+param[1]+"]",clients_num>max_clients_num*0.85); break; case "max_clients": max_clients_num=parseInt(param[1]); break; case "s": smeter_level=parseFloat(param[1]); setSmeterAbsoluteValue(smeter_level); break; } } /*} catch(err) { divlog("Received invalid message over WebSocket."); }*/ } } function add_problem(what) { problems_span=e("openwebrx-problems"); 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 } 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 = 4096;//2048 was choppy 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 = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); var audio_last_output_buffer = new Float32Array(audio_buffer_size); 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) { ws.send("SET "+what+"="+value.toString()); } function parsehash() { if(h=window.location.hash) { h.substring(1).split(",").forEach(function(x){ harr=x.split("="); console.log(harr); if(harr[0]=="mod") starting_mod = harr[1]; 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); } //we send our setup packet parsehash(); //needs audio_context.sampleRate to exist audio_calculate_resampling(audio_context.sampleRate); audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); ws.send("SET output_rate="+audio_server_output_rate.toString()+" action=start"); //now we'll get AUD packets as well } function audio_init() { 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) { 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. Please reload the page.", 1); } 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() { //if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost") //{ //divlog("Server administrator should set server_hostname correctly, because it is left as \"localhost\". Now guessing hostname from page URL.",1); 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+client_id); 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) { 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.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[index+1],waterfall_colors[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() { 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"); 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,900/fft_fps); resize_waterfall_container(false); /* then */ resize_canvases(); scale_setup(); mkzoomlevels(); waterfall_setup_done=1; } var waterfall_dont_scale=0; 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; }*/ //Add line to waterfall image oneline_image = canvas_context.createImageData(w,1); for(x=0;x>>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(); //divlog("Drawn FFT"); } /* 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"; }); } function openwebrx_resize() { resize_canvases(); resize_waterfall_container(true); resize_scale(); check_top_bar_congestion(); } function openwebrx_init() { if(ios) 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(); place_panels(); window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); window.addEventListener("resize",openwebrx_resize); check_top_bar_congestion(); //Synchronise volume with slider updateVolume(); waterfallColorsDefault(); } 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); } /* 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) { 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; } 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; } } 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;i