diff --git a/README.md b/README.md
index 1794031c175c04f8d21efa269b1e6c9950f32b91..a799180136a0bee55daaf904096a9bfd0c7fb2be 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,44 @@
-openwebrx
+OpenWebRX
 =========
 
-Open source web-based SDR receiver software
+OpenWebRX is a multi-user SDR receiver software with a web interface.
+
+![OpenWebRX](/screenshot.jpg?raw=true)
+
+It has the following features:
+
+- <a href="https://github.com/simonyiszk/csdr">libcsdr</a> based demodulators (AM/FM/SSB),
+- filter bandwith, BFO, PBS can be set from GUI,
+- waterfall display can be shifted back in time,
+- it extensively uses HTML5 features like WebSocket, Web Audio API, and &gt;canvas&lt;.
+- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28),
+- currently only supports RTL-SDR, but other SDR hardware may be easily added.
+
+## Setup
+
+OpenWebRX currently requires a Linux machine to run. 
+
+First you will need to install the dependencies:
+- <a href="https://github.com/simonyiszk/csdr">libcsdr</a>
+- <a href="http://sdr.osmocom.org/trac/wiki/rtl-sdr">rtl-sdr</a>
+
+After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
+
+	python openwebrx.py
+
+You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>.
+
+Please note that it is also listening on the following ports (on localhost only):
+- port 8888 for the I/Q source,
+- port 4951 for the multi-user I/Q server.
+
+Now the next step is to customize the parameters of your server in `config_webrx.py`.
+
+Actually, if you do something cool with OpenWebRX (or just have a problem), please drop me a mail: Andras Retzler, HA7ILM &gt;randras@sdr.hu&lt;.
+I would like to maintain a list of online amateur radio receivers on <a href="http://sdr.hu/">sdr.hu</a>.
+
+## Usage tips
+
+The filter envelope can be dragged at its ends and moved.
+
+However, if you hold the shift key, you can drag the center line (BFO) or the passband (PBS).
diff --git a/config_rtl.py b/config_rtl.py
new file mode 100755
index 0000000000000000000000000000000000000000..c7599783bfa0c4e6bf9c92940a71ae9c78dc22cb
--- /dev/null
+++ b/config_rtl.py
@@ -0,0 +1,87 @@
+'''
+This file is part of RTL Multi-User Server, 
+	that makes multi-user access to your DVB-T dongle used as an SDR.
+Copyright (c) 2013-2014 by Andras Retzler <randras@sdr.hu>
+
+RTL Multi-User Server is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RTL Multi-User Server 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with RTL Multi-User Server.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+my_ip='127.0.0.1' # leave blank for listening on all interfaces
+my_listening_port = 4951 
+
+rtl_tcp_host,rtl_tcp_port='localhost',8888
+
+send_first=""
+#send_first=chr(9)+chr(0)+chr(0)+chr(0)+chr(1) # set direct sampling
+
+setuid_on_start = 0					# we normally start with root privileges and setuid() to another user
+uid = 999 							# determine by issuing: $ id -u username
+ignore_clients_without_commands = 1 # we won't serve data to telnet sessions and things like that
+									# we'll start to serve data after getting the first valid command 
+
+freq_allowed_ranges = [[0,2200000000]]
+
+client_cant_set_until=0		
+first_client_can_set=True	# openwebrx - spectrum thread will set things on start # no good, clients set parameters and things
+buffer_size=25000000		# per client
+log_file_path = "/dev/null" # Might be set to /dev/null to turn off logging
+
+'''
+Allow any host to connect:
+	use_ip_access_control=0
+
+Allow from specific ranges:
+	use_ip_access_control=1
+	order_allow_deny=0 # deny and then allow
+	denied_ip_ranges=() # deny from all
+	allowed_ip_ranges=('192.168.','44.','127.0.0.1') # allow only from ...
+
+Deny from specific ranges:
+	use_ip_access_control=1
+	order_allow_deny=0 # allow and then deny
+	allowed_ip_ranges=() # allow from all
+	denied_ip_ranges=('192.168.') # deny any hosts from ...
+'''
+use_ip_access_control=1 #You may want to open up the I/Q server to the public, then set this to zero.
+order_allow_deny=0
+denied_ip_ranges=() # deny from all
+allowed_ip_ranges=('127.0.0.1') # allow only local connections (from openwebrx). 
+allow_gain_set=1
+
+use_dsp_command=False # you can process raw I/Q data with a custom command that starts a process that we can pipe the data into, and also pipe out of.
+debug_dsp_command=False # show sample rate before and after the dsp command
+dsp_command=""
+
+'''
+Example DSP commands:
+  * Compress I/Q data with FLAC:
+    flac --force-raw-format --channels 2 --sample-rate=250000 --sign=unsigned --bps=8 --endian=little -o - -
+  * Decompress FLAC-coded I/Q data:
+    flac --force-raw-format --decode --endian=little --sign=unsigned - -
+'''
+watchdog_interval=1.5
+reconnect_interval=10 
+'''
+If there's no input I/Q data after N seconds, input will be filled with zero samples, 
+so that GNU Radio won't fail in openwebrx. It may reconnect rtl_tcp_tread. 
+If watchdog_interval is 0, then watchdog thread is not started. 
+
+'''
+cache_full_behaviour=2
+'''
+	0 = drop samples
+	1 = close client
+	2 = openwebrx: don't care about that client until it wants samples again (gr-osmosdr bug workaround)
+'''
+
diff --git a/config_webrx.py b/config_webrx.py
new file mode 100755
index 0000000000000000000000000000000000000000..2565f54b5515db07af6bcfba5dce618c77ded8c2
--- /dev/null
+++ b/config_webrx.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*- 
+
+"""
+config_webrx: configuration options for OpenWebRX
+
+OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+#Server settings
+web_port=8073
+server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
+
+#Web GUI configuration
+receiver_name="[Callsign]"
+receiver_location="Budapest, Hungary"
+receiver_qra="JN97ML"
+receiver_asl=182
+receiver_ant="Longwire"
+receiver_device="RTL-SDR"
+receiver_admin="localhost@localhost"
+receiver_gps=(47.000000,19.000000)
+photo_height=350
+photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory"
+photo_desc="""
+You can add your own background photo and receiver information.<br />
+Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/>
+Device: %[RX_DEVICE]<br />
+Antenna: %[RX_ANT]<br />
+Website: <a href="http://localhost" target="_blank">http://localhost</a>
+"""
+
+#DSP/RX settings
+dsp_plugin="csdr"
+fft_fps=9
+fft_size=4096
+samp_rate = 250000
+center_freq = 145525000
+rf_gain = 5
+
+start_rtl_thread=True #rtl_sdr is more stable than rtl_tcp...
+start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq}  - | nc -vvl 127.0.0.1 8888".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) 
+#start_rtl_tcp_command="rtl_tcp -s 250000 -f 145525000 -g 0 -p 8888"
+#You can use other SDR hardware as well, but if the command above outputs samples in a format other than [unsigned char], then the dsp plugin has to be slightly modified (at the csdr convert_u8_f part).
+
diff --git a/htdocs/favicon.ico b/htdocs/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f8c9a2ae9562c79442e884e6e4b23fc43ec14f42
Binary files /dev/null and b/htdocs/favicon.ico differ
diff --git a/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf b/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..dfc87f87142837d8302744e2e8759980dc29e9f6
Binary files /dev/null and b/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf differ
diff --git a/htdocs/gfx/font-expletus-sans/OFL.txt b/htdocs/gfx/font-expletus-sans/OFL.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5979654eea178dfab1964398c737c9ea68becf6a
--- /dev/null
+++ b/htdocs/gfx/font-expletus-sans/OFL.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2011, Jasper de Waard (jasper@designtown.nl),
+with Reserved Font Name "Expletus Sans".
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/htdocs/gfx/openwebrx-avatar-background.png b/htdocs/gfx/openwebrx-avatar-background.png
new file mode 100755
index 0000000000000000000000000000000000000000..e52cb0b951a27ae1e759d229c802e98b16ae5067
Binary files /dev/null and b/htdocs/gfx/openwebrx-avatar-background.png differ
diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png
new file mode 100755
index 0000000000000000000000000000000000000000..91486a61d0d47522ce07d48c579771a825f9efef
Binary files /dev/null and b/htdocs/gfx/openwebrx-avatar.png differ
diff --git a/htdocs/gfx/openwebrx-background-cool-blue.png b/htdocs/gfx/openwebrx-background-cool-blue.png
new file mode 100755
index 0000000000000000000000000000000000000000..7430bd8a8461217a5c07ef86558afc2bcfcf98c3
Binary files /dev/null and b/htdocs/gfx/openwebrx-background-cool-blue.png differ
diff --git a/htdocs/gfx/openwebrx-background-lingrad.png b/htdocs/gfx/openwebrx-background-lingrad.png
new file mode 100755
index 0000000000000000000000000000000000000000..48537f7a0ffbe3921191cda68f325e30420b5943
Binary files /dev/null and b/htdocs/gfx/openwebrx-background-lingrad.png differ
diff --git a/htdocs/gfx/openwebrx-logo-big.png b/htdocs/gfx/openwebrx-logo-big.png
new file mode 100755
index 0000000000000000000000000000000000000000..dcafb2ee3778bc1d880ab32e650d36fab91b0957
Binary files /dev/null and b/htdocs/gfx/openwebrx-logo-big.png differ
diff --git a/htdocs/gfx/openwebrx-rx-details-arrow-up.png b/htdocs/gfx/openwebrx-rx-details-arrow-up.png
new file mode 100755
index 0000000000000000000000000000000000000000..0baccd041dbba4f6807b3ba87436f6998af9e540
Binary files /dev/null and b/htdocs/gfx/openwebrx-rx-details-arrow-up.png differ
diff --git a/htdocs/gfx/openwebrx-rx-details-arrow.png b/htdocs/gfx/openwebrx-rx-details-arrow.png
new file mode 100755
index 0000000000000000000000000000000000000000..9995118f0d1c4e705687a3ed9a5dfe7952e6e13c
Binary files /dev/null and b/htdocs/gfx/openwebrx-rx-details-arrow.png differ
diff --git a/htdocs/gfx/openwebrx-scale-background.png b/htdocs/gfx/openwebrx-scale-background.png
new file mode 100755
index 0000000000000000000000000000000000000000..7fbb4d2490317a99bde1d4882e72713ae0ce31f4
Binary files /dev/null and b/htdocs/gfx/openwebrx-scale-background.png differ
diff --git a/htdocs/gfx/openwebrx-top-logo.png b/htdocs/gfx/openwebrx-top-logo.png
new file mode 100755
index 0000000000000000000000000000000000000000..477242524b9434230467dcccadfbd489c37607e0
Binary files /dev/null and b/htdocs/gfx/openwebrx-top-logo.png differ
diff --git a/htdocs/gfx/openwebrx-top-photo.jpg b/htdocs/gfx/openwebrx-top-photo.jpg
new file mode 100755
index 0000000000000000000000000000000000000000..cf521c75251ca17ab939a00643212f61358c662d
Binary files /dev/null and b/htdocs/gfx/openwebrx-top-photo.jpg differ
diff --git a/htdocs/gfx/webrx-ha5kfu-top-logo.png b/htdocs/gfx/webrx-ha5kfu-top-logo.png
new file mode 100755
index 0000000000000000000000000000000000000000..2686eef0aa16eceddfa68cc79dbcd626de4bec4d
Binary files /dev/null and b/htdocs/gfx/webrx-ha5kfu-top-logo.png differ
diff --git a/htdocs/index.wrx b/htdocs/index.wrx
new file mode 100755
index 0000000000000000000000000000000000000000..7cf73ba733e643668b38f905c3fda56250daff08
--- /dev/null
+++ b/htdocs/index.wrx
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<!--
+OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX. If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+	<head>
+		<title>OpenWebRX | Open Source Web-based SDR for everyone!</title>
+		<script type="text/javascript">
+			//Local variables
+			client_id="%[CLIENT_ID]";
+			ws_url="%[WS_URL]";
+			rx_photo_height=%[RX_PHOTO_HEIGHT];
+		</script>
+		<script src="openwebrx.js"></script>
+		<link rel="stylesheet" type="text/css" href="openwebrx.css" />
+		<meta charset="utf-8">
+	</head>
+	<body onload="openwebrx_init();">
+<div id="webrx-page-container">
+	<div id="webrx-top-container">
+		<div id="webrx-top-photo-clip"> 
+			<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> 
+			<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
+			<div id="webrx-rx-photo-desc">%[RX_PHOTO_DESC]</div>
+		</div>
+		<div id="webrx-top-bar-background" class="webrx-top-bar-parts"></div>
+		<div id="webrx-top-bar" class="webrx-top-bar-parts">
+			<a href="http://openwebrx.org/" target="_blank"><img src="gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
+			<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
+			<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/>
+			<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
+			<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</div>
+			<div id="webrx-rx-desc" onclick="toggle_rx_photo();">%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, <a href="https://www.google.hu/maps/place/%[RX_GPS]" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a></div>
+			<div id="openwebrx-rx-details-arrow">
+				<a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow-up.png" /></a>
+				<a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow.png" /></a>
+			</div>
+		</div>
+	</div>
+	<div id="webrx-main-container">
+			<div id="openwebrx-scale-container">
+				<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
+			</div>
+			<div id="webrx-canvas-container">
+				<div id="openwebrx-phantom-canvas"></div>
+				<!-- add canvas here by javascript -->
+			</div>
+			<div id="openwebrx-panels-container">
+				<div class="openwebrx-panel" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="215,70">
+					<div id="webrx-actual-freq">---.--- MHz</div>
+					<div id="webrx-mouse-freq">---.--- MHz</div>
+					<!--<div class="openwebrx-button" onclick="ws.send('SET mod=wfm');" >WFM</div>-->
+					<div class="openwebrx-button" onclick="demodulator_analog_replace('nfm');">FM</div>
+					<div class="openwebrx-button" onclick="demodulator_analog_replace('am');">AM</div>					
+					<div class="openwebrx-button" onclick="demodulator_analog_replace('lsb');">LSB</div>
+					<div class="openwebrx-button" onclick="demodulator_analog_replace('usb');">USB</div>
+					<div class="openwebrx-button" onclick="demodulator_analog_replace('cw');">CW</div>
+				</div>
+				<div class="openwebrx-panel" id="webrx-config" data-panel-name="debug" data-panel-pos="left" data-panel-order="0" data-panel-size="585,130">
+					<div class="openwebrx-panel-inner">
+						<div id="openwebrx-client-log-title">openwebrx.js (beta) client log </strong><span id="openwebrx-problems"></span></div>
+						Author: <a href="javascript:sendmail2('pi7qtu=alz$pc');">HA7ILM</a>. Please send me bug reports and suggestions.<br/>
+						Client status: <span id="openwebrx-client-status">
+							<span id="openwebrx-audio-sps"></span><br/>
+						<!--Server status: <span id="openwebrx-server-status">no information</span><br/>-->
+						Your client ID is: <em>%[CLIENT_ID]</em><br />
+						<div id="openwebrx-debugdiv"></div>
+					</div>
+				</div>
+				<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="none" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;">
+					<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
+					<br />We're working on the code right now, so the application might fail.
+				</div>
+			</div>
+	</div>
+</div>
+	</body>
+</html>
diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css
new file mode 100755
index 0000000000000000000000000000000000000000..15993fc4570f3af69cd682c3908171337e12fb26
--- /dev/null
+++ b/htdocs/openwebrx.css
@@ -0,0 +1,433 @@
+/*
+OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+html, body
+{
+	margin: 0;
+	padding: 0;
+   height: 100%;
+	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+	overflow: hidden;
+}
+
+#webrx-top-container
+{
+	position: relative;
+	z-index:1000;
+}
+
+.webrx-top-bar-parts
+{
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	width:100%;
+	height:67px;
+}
+
+#webrx-top-bar-background
+{
+	background-color: #808080;
+	opacity: 0.15;
+	filter:alpha(opacity=15);
+}
+
+#webrx-top-bar
+{
+	margin:0;
+	padding:0;
+}
+
+
+#webrx-top-logo
+{
+	position: absolute;
+	top: 12px;
+	left: 15px;
+}
+
+#webrx-ha5kfu-top-logo
+{
+	position: absolute;
+	top: 19px;
+	right: 15px;
+}
+
+#webrx-top-photo
+{
+	width: 100%;
+	display: block;
+}
+
+#webrx-rx-avatar-background
+{
+	cursor:pointer;
+	position: absolute;
+	left: 285px;
+	top: 6px;
+}
+
+#webrx-rx-avatar
+{
+	cursor:pointer;
+	position: absolute;
+	left: 289px;
+	top: 10px;
+	width: 46px;
+	height: 46px;
+}
+
+#webrx-top-photo-clip
+{
+	max-height: 350px;
+	overflow: hidden;
+	position: relative;
+}
+
+/*#webrx-bottom-bar
+{
+	position: absolute;
+	bottom: 0px;
+	width: 100%;
+	height: 117px;
+	background-image:url(gfx/webrx-bottom-bar.png);
+}*/
+
+#webrx-page-container
+{
+   min-height:100%;
+   position:relative;
+}
+
+/*#webrx-photo-gradient-left
+{
+	position: absolute;
+	bottom: 0px;
+	left: 0px;
+	background-image:url(gfx/webrx-photo-gradient-corner.png);	
+	width: 59px;
+	height: 92px;
+
+}
+
+#webrx-photo-gradient-middle
+{
+	position: absolute;
+	bottom: 0px;
+	left: 59px;
+	right: 59px;
+	height: 92px;
+	background-image:url(gfx/webrx-photo-gradient-middle.png);	
+}
+
+#webrx-photo-gradient-right
+{
+	position: absolute;
+	bottom: 0px;
+	right: 0px;
+	background-image:url(gfx/webrx-photo-gradient-corner.png);	
+	width: 59px;
+	height: 92px;
+   -webkit-transform:scaleX(-1);
+   -moz-transform:scaleX(-1);
+   -ms-transform:scaleX(-1);
+   -o-transform:scaleX(-1);
+   transform:scaleX(-1);
+}*/
+
+#webrx-rx-photo-title
+{
+	position: absolute;
+	left: 15px;
+	top: 78px;
+	color: White;
+	font-size: 16pt;
+	text-shadow: 1px 1px 4px #444;
+	opacity: 1;
+}
+
+#webrx-rx-photo-desc
+{
+	position: absolute;
+	left: 15px;
+	top: 109px;
+	color: White;
+	font-size: 10pt;
+	font-weight: bold;
+	text-shadow: 0px 0px 6px #444;
+	opacity: 1;
+	line-height: 1.5em;
+}
+
+#webrx-rx-photo-desc a
+{
+	color: #5ca8ff;
+	text-shadow: none;
+}
+
+#webrx-rx-title
+{
+	white-space:nowrap;
+	overflow: hidden;
+	cursor:pointer;
+	position: absolute;
+	left: 350px;
+	top: 13px;
+	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+	color: #909090;
+	font-size: 11pt;
+	font-weight: bold;
+}
+
+#webrx-rx-desc
+{
+	white-space:nowrap;
+	overflow: hidden;
+	cursor:pointer;
+	font-size: 10pt;
+	color: #909090;
+	position: absolute;
+	left: 350px;
+	top: 34px;
+}
+
+#webrx-rx-desc a
+{
+	color: #909090;
+	/*text-decoration: none;*/
+}
+
+#openwebrx-rx-details-arrow
+{
+	cursor:pointer;
+	position: absolute;
+	left: 470px;
+	top: 51px;
+}
+
+#openwebrx-rx-details-arrow a
+{
+	margin: 0;
+	padding: 0;
+}
+
+#openwebrx-rx-details-arrow-down
+{
+	display:none;
+}
+
+/*canvas#waterfall-canvas
+{
+	border-style: none;
+	border-width: 1px;
+	height: 150px;
+	width: 100%;
+}*/
+
+#openwebrx-scale-container
+{
+	height: 47px;
+	background-image: url("gfx/openwebrx-scale-background.png");
+	background-repeat: repeat-x;
+	overflow: hidden;
+	z-index:1000;
+	position: relative;
+}
+
+#webrx-canvas-container
+{
+	/*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/
+	position: relative;
+	height: 2000px;
+	overflow-y: scroll;
+	overflow-x: hidden;
+	/*background-color: #646464;*/
+	/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/
+	background-image: url('gfx/openwebrx-background-cool-blue.png');
+	background-repeat: no-repeat;
+	background-color: #1e5f7f;
+	cursor: crosshair;
+}
+
+#webrx-canvas-container canvas
+{
+	position: absolute;
+	border-style: none;
+}
+
+#openwebrx-phantom-canvas
+{
+	position: absolute;
+	width: 0px;
+	height: 0px;
+}
+
+/*#openwebrx-canvas-gradient-background
+{
+	overflow: hidden;
+	width: 100%;
+	height: 396px;
+}*/
+
+/*#webrx-debugdiv
+{
+	font-size: 10pt;
+	/*overflow-y:scroll;*/
+/*}*/
+
+#webrx-main-container
+{
+	position: relative;
+	width: 100%;
+	margin: 0;
+	padding: 0;
+}
+
+.webrx-error
+{
+	font-weight: bold;
+	color: #ff6262;
+}
+
+#openwebrx-problems span
+{
+	background: #ff6262;
+	padding: 3px;
+	font-size: 8pt;
+	color: white;
+	font-weight: bold;
+	border-radius: 4px;
+	-moz-border-radius: 4px;
+	margin: 0px 2px 0px 2px;
+}
+
+/*#webrx-freq-show
+{
+	visibility: hidden;
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	padding: 5px;
+	font-weight: bold;
+	border-radius: 10px;
+	-moz-border-radius: 10px;
+	background-color: #999999;
+	color: White;
+	z-index:9999; /*should be higher?
+	
+}*/
+
+/* removed non-free fonts like that: */
+/*@font-face {
+    font-family: 'unibody_8_pro_regregular';
+    src: url('gfx/unibody8pro-regular-webfont.eot');
+    src: url('gfx/unibody8pro-regular-webfont.ttf');
+    font-weight: normal;
+    font-style: normal;
+}*/
+
+@font-face {
+    font-family: 'expletus-sans-medium';
+    src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
+    font-weight: normal;
+    font-style: normal;
+}
+
+#webrx-actual-freq
+{
+	width: 100%;
+	text-align: left;
+	font-size: 16pt;
+	font-family: 'expletus-sans-medium';
+	padding: 0;
+	margin: 0;
+	line-height:22px;
+	
+}
+
+#webrx-mouse-freq
+{
+	width: 100%;
+	text-align: left;
+	font-size: 10pt;
+	color: #AAA;
+	font-family: 'expletus-sans-medium';
+	margin-bottom: 5px;
+}
+
+.openwebrx-panel
+{
+	visibility: hidden;
+	background-color: #575757;
+	padding: 10px;
+	color: white;
+	position: fixed;
+	font-size: 10pt;
+	border-radius: 15px;
+	-moz-border-radius: 15px;
+}
+
+.openwebrx-panel a
+{
+	color: #5ca8ff;
+	text-shadow: none;
+}
+
+.openwebrx-panel-inner
+{
+	overflow-y: auto;
+	overflow-x: hidden;
+	height: 100%;
+}
+
+.openwebrx-button
+{
+	background-color: #373737;
+	padding: 5px;
+	border-radius: 5px;
+	-moz-border-radius: 5px;
+	color: White;
+	font-weight: bold;
+	width: auto;
+	float: left;
+	margin-right: 5px;
+	cursor: pointer;
+	background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0	, #373737), color-stop(1, #4F4F4F) );
+	background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
+}
+
+.openwebrx-button:hover
+{
+	/*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0	, #3F3F3F), color-stop(1, #777777) );
+	background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/
+	background: #474747;
+	color: #FFFF50;
+}
+
+.openwebrx-button:active 
+{
+	background: #777777;
+	color: #FFFF50;	
+}
+
+#openwebrx-client-log-title 
+{
+	margin-bottom: 5px;
+	font-weight: bold;
+}
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js
new file mode 100755
index 0000000000000000000000000000000000000000..381da83dade6b227c28daabc52b4a158be78a8d9
--- /dev/null
+++ b/htdocs/openwebrx.js
@@ -0,0 +1,1634 @@
+/*
+
+OpenWebRX (c) Copyright 2013-2014 Andras Retzler <randras@sdr.hu>
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+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<num;i++) output+=String.fromCharCode(u8buf[i]);
+	return output;
+}
+
+var bandwidth;
+var center_freq;
+var audio_buffer_current_size_debug=0;
+var audio_buffer_all_size_debug=0;
+var audio_buffer_current_count_debug=0;
+var audio_buffer_current_size=0;
+var fft_size;
+var fft_fps;
+var waterfall_setup_done=0;
+var waterfall_queue = [];
+var waterfall_timer;
+
+/*function fade(something,from,to,time_ms,fps)
+{
+	something.style.opacity=from;
+	something.fade_i=0;
+	n_of_iters=time_ms/(1000/fps);
+	change=(to-from)/(n_of_iters-1);
+	
+	something.fade_timer=window.setInterval(
+		function(){
+			if(something.fade_i++<n_of_iters)
+				something.style.opacity=parseFloat(something.style.opacity)+change;
+			else 
+				{something.style.opacity=to; window.clearInterval(something.fade_timer); }
+		},1000/fps);
+}*/
+
+var rx_photo_state=1;
+
+function e(what) { return document.getElementById(what); }
+
+function init_rx_photo()
+{
+	e("webrx-top-photo-clip").style.maxHeight=rx_photo_height.toString()+"px";
+	window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000);
+	window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500);
+	window.setTimeout(function() { close_rx_photo() },2500);
+}
+
+dont_toggle_rx_photo_flag=0;
+
+function dont_toggle_rx_photo()
+{
+	dont_toggle_rx_photo_flag=1;
+}
+
+function toggle_rx_photo()
+{
+	if(dont_toggle_rx_photo_flag) { dont_toggle_rx_photo_flag=0; return; }
+	if(rx_photo_state) close_rx_photo();
+	else open_rx_photo()
+}
+
+function close_rx_photo()
+{
+	rx_photo_state=0;
+	animate_to(e("webrx-top-photo-clip"),"maxHeight","px",67,0.93,1000,60,function(){resize_waterfall_container(true);});
+	e("openwebrx-rx-details-arrow-down").style.display="block";
+	e("openwebrx-rx-details-arrow-up").style.display="none";
+}
+
+function open_rx_photo()
+{
+	rx_photo_state=1;
+	e("webrx-rx-photo-desc").style.opacity=1;
+	e("webrx-rx-photo-title").style.opacity=1;
+	animate_to(e("webrx-top-photo-clip"),"maxHeight","px",rx_photo_height,0.93,1000,60,function(){resize_waterfall_container(true);});
+	e("openwebrx-rx-details-arrow-down").style.display="none";
+	e("openwebrx-rx-details-arrow-up").style.display="block";
+}
+
+function style_value(of_what,which)
+{
+	if(of_what.currentStyle) return of_what.currentStyle[which];
+	else if (window.getComputedStyle) return document.defaultView.getComputedStyle(of_what,null).getPropertyValue(which); 	
+}
+
+// ========================================================
+// =================  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++<n_of_iters)
+			{
+				if(accel==1) object.style[style_name]=(parseFloat(object.style[style_name])+change).toString()+unit;
+				else 
+				{ 
+					remain=parseFloat(object.style[style_name])-to;
+					if(Math.abs(remain)>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_px<from_px) /* swap'em */ { temp_px=to_px; to_px=from_px; from_px=temp_px; }
+	
+	/*from_px-=env_bounding_line_w/2;
+	to_px+=env_bounding_line_w/2;*/
+	from_px-=(env_att_w+env_bounding_line_w);
+	to_px+=(env_att_w+env_bounding_line_w); 
+	// do drawing:
+	scale_ctx.lineWidth=3;
+	scale_ctx.strokeStyle=color;
+	scale_ctx.fillStyle = color;
+	var drag_ranges={ envelope_on_screen: false, line_on_screen: false };
+	if(!(to_px<0||from_px>window.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=100; 
+//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_context.sampleRate/2,
+		low_cut_limit: -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.filter.low_cut_limit) return true;
+			//nor the filter passband be too small
+			if(this.parent.high_cut-new_value<this.parent.filter.min_passband) return true; 
+			//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
+			if(new_value>=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_cut<this.parent.filter.min_passband) return true; 
+			//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
+			if(new_value<=this.parent.low_cut) return true;
+			this.parent.high_cut=new_value;
+		}
+		if(this.dragged_range==dr.anything_else||this.dragged_range==dr.bfo)
+		{
+			//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
+			new_value=this.drag_origin.offset_frequency+freq_change;
+			if(new_value>bandwidth/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;i<demodulators.length;i++)
+	{
+		demodulators[i].envelope.draw(visible_range);
+	}
+}
+
+function demodulator_remove(which)
+{
+	demodulators[which].stop();
+	demodulators.splice(which,1);
+}
+
+function demodulator_add(what)
+{
+	demodulators.push(what);
+	mkenvelopes(get_visible_freq_range());
+}
+
+function demodulator_analog_replace(subtype)
+{ //this function should only exist until the multi-demodulator capability is added	
+	var temp_offset=0;
+	if(demodulators.length) 
+	{
+		temp_offset=demodulators[0].offset_frequency;
+		demodulator_remove(0);
+	}
+	demodulator_add(new demodulator_default_analog(temp_offset,subtype));
+}
+
+function demodulator_set_offset_frequency(which,to_what)
+{
+	if(to_what>bandwidth/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<demodulators.length;i++) event_handled|=demodulators[i].envelope.drag_start(evt.pageX,scale_canvas_drag_params.key_modifiers);
+		scale_canvas.style.cursor="move";
+	}
+	else if(scale_canvas_drag_params.drag)
+	{
+		//call the drag_move for all demodulators (and they will decide if they're dragged)
+		for (var i=0;i<demodulators.length;i++) event_handled|=demodulators[i].envelope.drag_move(evt.pageX);
+		if (!event_handled) demodulator_set_offset_frequency(0,scale_offset_freq_from_px(evt.pageX));
+	}
+	
+}
+
+function scale_canvas_end_drag(x)
+{
+	canvas_container.style.cursor="default";
+	scale_canvas_drag_params.drag=false;
+	scale_canvas_drag_params.mouse_down=false;
+	var event_handled=false;
+	for (var i=0;i<demodulators.length;i++) event_handled|=demodulators[i].envelope.drag_end(x);
+	//console.log(event_handled);
+	if (!event_handled) demodulator_set_offset_frequency(0,scale_offset_freq_from_px(x));
+}
+
+function scale_canvas_mouseup(evt)
+{
+	scale_canvas_end_drag(evt.pageX);
+}
+
+function scale_px_from_freq(f,range) { return Math.round(((f-range.start)/range.bw)*canvas_container.clientWidth); }
+
+function get_visible_freq_range()
+{
+	out={};
+	fcalc=function(x) { return Math.round(((-zoom_offset_px+x)/canvases[0].clientWidth)*bandwidth)+(center_freq-bandwidth/2); }
+	out.start=fcalc(0);
+	out.center=fcalc(canvas_container.clientWidth/2);
+	out.end=fcalc(canvas_container.clientWidth);
+	out.bw=out.end-out.start;
+	out.hps=out.bw/canvas_container.clientWidth;
+	return out;
+}
+
+var scale_markers_levels=[
+	{
+		"large_marker_per_hz":10000000, //large
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":0
+	},
+	{
+		"large_marker_per_hz":5000000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":0
+	},
+	{
+		"large_marker_per_hz":1000000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":0
+	},
+	{
+		"large_marker_per_hz":500000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":1
+	},
+	{
+		"large_marker_per_hz":100000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":1
+	},
+	{
+		"large_marker_per_hz":50000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":2
+	},
+	{
+		"large_marker_per_hz":10000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":2
+	},
+	{
+		"large_marker_per_hz":5000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":3
+	},
+	{
+		"large_marker_per_hz":1000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":1
+	}
+];
+var scale_min_space_bw_texts=50;
+var scale_min_space_bw_small_markers=7;
+
+function get_scale_mark_spacing(range)
+{
+	out={};
+	fcalc=function(freq) 
+	{ 
+		out.numlarge=(range.bw/freq);
+		out.large=canvas_container.clientWidth/out.numlarge; 	//distance between large markers (these have text)
+		out.ratio=5; 														//(ratio-1) small markers exist per large marker
+		out.small=out.large/out.ratio; 								//distance between small markers
+		if(out.small<scale_min_space_bw_small_markers) return false; 
+		if(out.small/2>=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; }
+		out.smallbw=freq/out.ratio;
+		return true;
+	}
+	for(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)
+			{ //if this is the first overall marker when zoomed out
+				if(x<text_measured.width/2)
+				{ //and if it would be clipped off the screen
+					if(scale_px_from_freq(marker_hz+spacing.smallbw*spacing.ratio,range)-text_measured.width>=scale_min_space_bw_texts)
+					{ //and if we have enough space to draw it correctly without clipping
+						scale_ctx.textAlign = "left";
+						scale_ctx.fillText(text_to_draw, 0, text_h_pos); 
+					}
+				}
+			}
+			else if(zoom_level==0&&range.end-spacing.smallbw*spacing.ratio<marker_hz)  
+			{ //if this is the last overall marker when zoomed out
+				if(x>window.innerWidth-text_measured.width/2) 
+				{ //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/2<window.innerWidth) scale_ctx.fillText(text_to_draw, x, 22+10);
+	}
+}
+
+function resize_scale()
+{
+	scale_ctx.canvas.width  = window.innerWidth;
+	scale_ctx.canvas.height = 47;
+	mkscale();
+}
+
+function canvas_mouseover(evt)
+{
+	if(!waterfall_setup_done) return;
+	//e("webrx-freq-show").style.visibility="visible";	
+}
+
+function canvas_mouseout(evt)
+{
+	if(!waterfall_setup_done) return;
+	//e("webrx-freq-show").style.visibility="hidden";
+}
+
+function canvas_get_freq_offset(relativeX)
+{
+	rel=(relativeX/canvases[0].clientWidth);
+	return Math.round((bandwidth*rel)-(bandwidth/2));
+}
+
+function canvas_get_frequency(relativeX)
+{
+	return center_freq+canvas_get_freq_offset(relativeX);
+}
+
+/*function canvas_format_frequency(relativeX)
+{
+	return (canvas_get_frequency(relativeX)/1e6).toFixed(3)+" MHz";
+}*/
+
+function format_frequency(format, freq_hz, pre_divide, decimals)
+{
+	out=format.replace("{x}",(freq_hz/pre_divide).toFixed(decimals));
+	at=out.indexOf(".")+4;
+	while(decimals>3)
+	{
+		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());
+		//e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4);
+		 demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX));		
+	}
+	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;
+
+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;i++)
+		zoom_levels.push(1+(maxc-1)*(i/(zoom_levels_count-1)));
+}
+
+function zoom_step(out, where, onscreen)
+{
+	if((out&&zoom_level==0)||(!out&&zoom_level>=zoom_levels_count-1)) return;
+	
+	if(out) --zoom_level;
+	else ++zoom_level;
+	zoom_center_rel=canvas_get_freq_offset(where);
+	//console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString());
+	zoom_center_where=onscreen;
+	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_px<winsize-canvases_new_width) 
+		zoom_offset_px=winsize-canvases_new_width;
+	//console.log("zoom_calc || zopx:"+zoom_offset_px.toString()+ " maxoff:"+(winsize-canvases_new_width).toString()+" relval:"+(0.5+zoom_center_rel/bandwidth).toString() );
+}
+
+function resize_waterfall_container(check_init)
+{
+	if(check_init&&!waterfall_setup_done) return;
+	canvas_container.style.height=(window.innerHeight-e("webrx-top-container").clientHeight-e("openwebrx-scale-container").clientHeight).toString()+"px";
+}
+
+
+function on_ws_recv(evt)
+{
+	if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; }
+	//
+	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=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(audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to) audio_init()
+	}
+	else if(firstChars=="FFT")
+	{
+		//alert("Yupee! Doing FFT");
+		var floatArray = new Float32Array(evt.data,4);
+		waterfall_add_queue(floatArray);
+	} else if(firstChars=="MSG")
+	{
+		/*try
+		{*/
+			var stringData=arrayBufferToString(evt.data);
+			params=stringData.substring(4).split(" ");
+			for(i=0;i<params.length;i++)
+			{
+				param=params[i].split("=");
+				switch(param[0])
+				{
+					case "setup":
+						waterfall_init();
+						break;					
+					case "bandwidth":
+						bandwidth=parseInt(param[1])
+						break;		
+					case "center_freq":
+						center_freq=parseInt(param[1])
+						break;
+					case "fft_size":
+						fft_size=parseInt(param[1])
+						break;
+					case "fft_fps":
+						fft_fps=parseInt(param[1])
+						break;
+
+				}
+			}
+		/*}
+		catch(err)
+		{
+			divlog("Received invalid message over WebSocket.");
+		}*/
+	}
+
+}
+
+function add_problem(what)
+{
+	problems_span=e("openwebrx-problems");
+	for(var i=0;i<problems_span.children.length;i++) if(problems_span.children[i].innerHTML==what) return;
+	new_span = document.createElement("span");
+	new_span.innerHTML=what;
+	problems_span.appendChild(new_span);
+	window.setTimeout(function(ps,ns) {  ps.removeChild(ns); }, 1000,problems_span,new_span);
+}
+
+function waterfall_add_queue(what)
+{
+	waterfall_queue.push(what);
+}
+
+function waterfall_dequeue()
+{
+	if(waterfall_queue.length) waterfall_add(waterfall_queue.shift());
+	if(waterfall_queue.length>fft_fps/2) //in case of emergency 
+	{
+		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);
+}
+
+function divlog(what, is_error)
+{
+	if(typeof is_error !== undefined && is_error == 1) what="<span class=\"webrx-error\">"+what+"</span>";
+	e("openwebrx-debugdiv").innerHTML+=what+"<br />";
+}
+
+var audio_context;
+var audio_initialized=0;
+
+var audio_received = Array();
+var audio_buffer_index = 0;
+var audio_resampler;
+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 = 8192;//2048 was choppy
+var audio_buffer_maximal_length_sec=1.7; //actual number of samples are calculated from sample rate
+var audio_flush_interval_ms=250; //the interval in which audio_flush() is called
+
+var audio_prepared_buffers = Array();
+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=10; //on audio underrun we wait until this n*audio_buffer_size samples are present
+
+function audio_prepare(data)
+{
+	//console.log("audio_prepare :: "+data.length.toString());
+	//console.log("data.len = "+data.length.toString());
+	var dopush=function()
+	{
+		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++;
+	};
+
+	if(data.length==0) return;
+	if(audio_last_output_offset+data.length<=audio_buffer_size)
+	{	//array fits into output buffer
+		for(var i=0;i<data.length;i++) audio_last_output_buffer[i+audio_last_output_offset]=data[i]/32768;
+		audio_last_output_offset+=data.length;
+		//console.log("fits into; offset="+audio_last_output_offset.toString());
+		if(audio_last_output_offset==audio_buffer_size) dopush();
+	}
+	else
+	{	//array is larger than the remaining space in the output buffer
+		var copied=audio_buffer_size-audio_last_output_offset;
+		var remain=data.length-copied;
+		for(var i=0;i<audio_buffer_size-audio_last_output_offset;i++) //fill the remaining space in the output buffer
+			audio_last_output_buffer[i+audio_last_output_offset]=data[i]/32768;
+		dopush();//push the output buffer and create a new one
+		//console.log("larger than; copied half: "+copied.toString()+", now at: "+audio_last_output_offset.toString());
+		for(var i=0;i<remain;i++) //copy the remaining input samples to the new output buffer
+			audio_last_output_buffer[i]=data[i+copied]/32768;
+		audio_last_output_offset+=remain;
+		//console.log("larger than; remained: "+remain.toString()+", now at: "+audio_last_output_offset.toString());
+	}
+	if(audio_buffering && audio_prepared_buffers.length>audio_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;i<input.length;i++) cd[i]=input[i];
+	}
+}
+
+function audio_onprocess(e)
+{	
+	if(audio_buffering) return;
+	if(audio_prepared_buffers.length==0) { add_problem("audio underrun"); audio_buffering=true; }
+	else e.outputBuffer.copyToChannel(audio_prepared_buffers.shift(),0);
+}
+
+
+
+
+function audio_flush()
+{
+	flushed=false;
+	while(audio_buffer_maximal_length_sec*audio_context.sampleRate<audio_prepared_buffers.length*audio_buffer_size)
+	{
+		flushed=true;
+		audio_prepared_buffers.shift();
+	}
+	if(flushed) add_problem("audio overrun");
+}
+
+
+function audio_onprocess_notused(e) 
+{
+	//https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js
+	if(audio_received.length==0) 
+	{ add_problem("audio underrun"); return; }
+	output = e.outputBuffer.getChannelData(0);
+	int_buffer = audio_received[0];
+	read_remain = audio_buffer_size;
+	//audio_buffer_maximal_length=120;
+
+	obi=0; //output buffer index
+	debug_str=""
+	while(1)	
+	{
+		if(int_buffer.length-audio_buffer_index>read_remain)
+		{
+			for (i=audio_buffer_index; i<audio_buffer_index+read_remain; i++)
+				output[obi++] = int_buffer[i]/32768;
+			//debug_str+="added whole ibl="+int_buffer.length.toString()+" abi="+audio_buffer_index.toString()+" "+(int_buffer.length-audio_buffer_index).toString()+">"+read_remain.toString()+" obi="+obi.toString()+"\n";
+			audio_buffer_index+=read_remain;
+			break;
+		}
+		else
+		{	
+			for (i=audio_buffer_index; i<int_buffer.length; i++)
+				output[obi++] = int_buffer[i]/32768;
+			read_remain-=(int_buffer.length-audio_buffer_index);
+			audio_buffer_current_size-=audio_received[0].length;
+			/*if (audio_received.length>audio_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 audio_init()
+{
+	//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
+	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);
+	}
+
+	//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
+	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('nfm'); //needs audio_context.sampleRate to exist
+}
+
+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);
+}
+
+function open_websocket()
+{
+	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;
+}
+
+//var color_scale=[0xFFFFFFFF, 0x000000FF];
+//var color_scale=[0x000000FF, 0x000000FF, 0x3a0090ff, 0x10c400ff, 0xffef00ff, 0xff5656ff];
+//var color_scale=[0x000000FF, 0x000000FF, 0x534b37ff, 0xcedffaff, 0x8899a9ff,  0xfff775ff, 0xff8a8aff, 0xb20000ff];
+
+//var color_scale=[ 0x000000FF, 0xff5656ff, 0xffffffff];
+
+//2014-04-22
+var color_scale=[0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff,  0xfff775ff, 0xff8a8aff, 0xb20000ff];
+
+function waterfall_mkcolor(db_value)
+{
+	min_value=-100; //in dB
+	max_value=10
+	if(db_value<min_value) db_value=min_value
+	if(db_value>max_value) db_value=max_value
+	full_scale=max_value-min_value;
+	relative_value=db_value-min_value;
+	value_percent=relative_value/full_scale;
+	percent_for_one_color=1/(color_scale.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(color_scale[index+1],color_scale[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;i<w;i++) scaled[i]=-100;
+	}
+	else
+	{
+		if ((to-from)==w)
+		{
+			scaled=data;
+		}
+		else if ((to-from)<w)
+		{	//make line bigger
+			pixel_per_point=w/(to-from);
+			scaled=Array();
+			j=0;
+			remain=pixel_per_point;
+			for(i=0; i<w; i++)
+			{
+				//thiscolor=data[j]*(remain-floor(remain))+data[j+1]*(1-(remain-floor(remain)))
+				//nextcolor=data[j+1]*(remain-floor(remain))+data[j+2]*(1-(remain-floor(remain)))
+				if(remain>1)
+				{
+					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; i<to; i++)
+			{
+				if(remain>1)
+				{
+					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<w;x++)
+	{
+		color=waterfall_mkcolor(scaled[x]);
+		for(i=0;i<4;i++)
+			waterfall_image.data[base+x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
+	}*/
+
+	//Add line to waterfall image			
+	oneline_image = canvas_context.createImageData(w,1);
+	for(x=0;x<w;x++)
+	{
+		color=waterfall_mkcolor(data[x]);
+		for(i=0;i<4;i++)
+			oneline_image.data[x*4+i] = ((color>>>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; y<h-1; y++)
+	{
+		for(i=0; i<w*4; i++)
+			waterfall_image.data[y*w*4+i] = waterfall_image.data[(y+1)*w*4+i];
+	}
+}*/
+
+function check_top_bar_congestion()
+{
+	var wt=e("webrx-rx-title");
+	var tl=e("webrx-ha5kfu-top-logo");
+	if(wt.offsetLeft+wt.offsetWidth>tl.offsetLeft-20) tl.style.display="none";
+	else tl.style.display="block";
+}
+
+function openwebrx_resize() 
+{
+	resize_canvases();
+	resize_waterfall_container(true);
+	resize_scale();
+	check_top_bar_congestion();
+}
+
+function openwebrx_init()
+{
+	init_rx_photo();
+	open_websocket();
+	place_panels();
+	window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000);
+	window.addEventListener("resize",openwebrx_resize);
+}
+
+/*
+window.setInterval(function(){ 
+	sum=0;
+	for(i=0;i<audio_received.length;i++)
+		sum+=audio_received[i].length;
+	divlog("audio buffer bytes: "+sum);
+}, 2000);*/
+
+/*function email(what)
+{
+	//| http://stackoverflow.com/questions/617647/where-is-my-one-line-implementation-of-rot13-in-javascript-going-wrong
+	what=what.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(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_taken=0;
+
+function debug_audio()
+{
+	audio_debug_time_taken+=1;
+	e("openwebrx-audio-sps").innerHTML=
+		"audio recv. at "+audio_buffer_current_size_debug.toString()+" sps ("+
+		(audio_buffer_all_size_debug/audio_debug_time_taken).toFixed(1)+" sps avg.), feed at "+
+		((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output";
+	audio_buffer_current_size_debug=0;
+}
+
+// ========================================================
+// =======================  PANELS  =======================
+// ========================================================
+
+panel_margin=10;
+
+function pop_bottommost_panel(from)
+{
+	min_order=parseInt(from[0].dataset.panelOrder);
+	min_index=0;
+	for(i=0;i<from.length;i++)	
+	{
+		actual_order=parseInt(from[i].dataset.panelOrder);
+		if(actual_order<min_order) 
+		{
+			min_index=i;
+			min_order=actual_order;
+		}
+	}
+	to_return=from[min_index];
+	from.splice(min_index,1);
+	return to_return;
+}
+
+function place_panels()
+{
+	var left_col=[];
+	var right_col=[];
+	var plist=e("openwebrx-panels-container").children;
+	for(i=0;i<plist.length;i++)
+	{
+		c=plist[i];
+		if(c.className=="openwebrx-panel")
+		{
+			newSize=c.dataset.panelSize.split(",");
+			if (c.dataset.panelPos=="left") { left_col.push(c); }
+			else if(c.dataset.panelPos=="right") { right_col.push(c); }
+			c.style.width=newSize[0]+"px";
+			c.style.height=newSize[1]+"px";
+			c.style.margin=panel_margin.toString()+"px";
+			c.openwebrxPanelWidth=parseInt(newSize[0]);			
+			c.openwebrxPanelHeight=parseInt(newSize[1]);
+		}
+	}
+	y=0;
+	while(left_col.length>0)
+	{
+		p=pop_bottommost_panel(left_col);
+		p.style.left="0px";
+		p.style.bottom=y.toString()+"px";
+		p.style.visibility="visible";
+		y+=p.openwebrxPanelHeight+3*panel_margin;
+	}
+	y=0;
+	while(right_col.length>0)
+	{
+		p=pop_bottommost_panel(right_col);
+		p.style.right="10px";
+		p.style.bottom=y.toString()+"px";
+		p.style.visibility="visible";
+		y+=p.openwebrxPanelHeight+3*panel_margin;
+	}
+}
+
diff --git a/htdocs/openwebrx.js~ b/htdocs/openwebrx.js~
new file mode 100755
index 0000000000000000000000000000000000000000..bb92e15a423e1da13835d4982bca5105dbf93d20
--- /dev/null
+++ b/htdocs/openwebrx.js~
@@ -0,0 +1,1538 @@
+/*
+
+OpenWebRX (c) Copyright 2013 Andras Retzler <ha7ilm@sdr.hu>
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+function arrayBufferToString(buf) {
+	//http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers
+	return String.fromCharCode.apply(null, new Uint8Array(buf));
+}
+
+//Chrome console:
+//ws.send("SET center_freq=100000000")
+
+var bandwidth;
+var center_freq;
+var audio_buffer_current_size_debug=0;
+var audio_buffer_current_size=0;
+var fft_size;
+var fft_fps;
+var waterfall_setup_done=0;
+var waterfall_queue = [];
+var waterfall_timer;
+
+/*function fade(something,from,to,time_ms,fps)
+{
+	something.style.opacity=from;
+	something.fade_i=0;
+	n_of_iters=time_ms/(1000/fps);
+	change=(to-from)/(n_of_iters-1);
+	
+	something.fade_timer=window.setInterval(
+		function(){
+			if(something.fade_i++<n_of_iters)
+				something.style.opacity=parseFloat(something.style.opacity)+change;
+			else 
+				{something.style.opacity=to; window.clearInterval(something.fade_timer); }
+		},1000/fps);
+}*/
+
+var rx_photo_state=1;
+
+function e(what) { return document.getElementById(what); }
+
+function init_rx_photo()
+{
+	e("webrx-top-photo-clip").style.maxHeight=rx_photo_height.toString()+"px";
+	window.setTimeout(function() { animate(e("webrx-rx-photo-title"),"opacity","",1,0,1,500,30); },1000);
+	window.setTimeout(function() { animate(e("webrx-rx-photo-desc"),"opacity","",1,0,1,500,30); },1500);
+	window.setTimeout(function() { close_rx_photo() },2500);
+}
+
+function toggle_rx_photo()
+{
+	if(rx_photo_state) close_rx_photo();
+	else open_rx_photo()
+}
+
+function close_rx_photo()
+{
+	rx_photo_state=0;
+	animate_to(e("webrx-top-photo-clip"),"maxHeight","px",67,0.93,1000,60,function(){resize_waterfall_container(true);});
+	e("openwebrx-rx-details-arrow-down").style.display="block";
+	e("openwebrx-rx-details-arrow-up").style.display="none";
+}
+
+function open_rx_photo()
+{
+	rx_photo_state=1;
+	e("webrx-rx-photo-desc").style.opacity=1;
+	e("webrx-rx-photo-title").style.opacity=1;
+	animate_to(e("webrx-top-photo-clip"),"maxHeight","px",rx_photo_height,0.93,1000,60,function(){resize_waterfall_container(true);});
+	e("openwebrx-rx-details-arrow-down").style.display="none";
+	e("openwebrx-rx-details-arrow-up").style.display="block";
+}
+
+function style_value(of_what,which)
+{
+	if(of_what.currentStyle) return of_what.currentStyle[which];
+	else if (window.getComputedStyle) return document.defaultView.getComputedStyle(of_what,null).getPropertyValue(which); 	
+}
+
+// ========================================================
+// =================  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++<n_of_iters)
+			{
+				if(accel==1) object.style[style_name]=(parseFloat(object.style[style_name])+change).toString()+unit;
+				else 
+				{ 
+					remain=parseFloat(object.style[style_name])-to;
+					if(Math.abs(remain)>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_px<from_px) /* swap'em */ { temp_px=to_px; to_px=from_px; from_px=temp_px; }
+	
+	/*from_px-=env_bounding_line_w/2;
+	to_px+=env_bounding_line_w/2;*/
+	from_px-=(env_att_w+env_bounding_line_w);
+	to_px+=(env_att_w+env_bounding_line_w); 
+	// do drawing:
+	scale_ctx.lineWidth=3;
+	scale_ctx.strokeStyle=color;
+	scale_ctx.fillStyle = color;
+	var drag_ranges={ envelope_on_screen: false, line_on_screen: false };
+	if(!(to_px<0||from_px>window.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=100; 
+//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_context.sampleRate/2,
+		low_cut_limit: -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.filter.low_cut_limit) return true;
+			//nor the filter passband be too small
+			if(this.parent.high_cut-new_value<this.parent.filter.min_passband) return true; 
+			//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
+			if(new_value>=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_cut<this.parent.filter.min_passband) return true; 
+			//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
+			if(new_value<=this.parent.low_cut) return true;
+			this.parent.high_cut=new_value;
+		}
+		if(this.dragged_range==dr.anything_else||this.dragged_range==dr.bfo)
+		{
+			//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
+			new_value=this.drag_origin.offset_frequency+freq_change;
+			if(new_value>bandwidth/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;i<demodulators.length;i++)
+	{
+		demodulators[i].envelope.draw(visible_range);
+	}
+}
+
+function demodulator_remove(which)
+{
+	demodulators[which].stop();
+	demodulators.splice(which,1);
+}
+
+function demodulator_add(what)
+{
+	demodulators.push(what);
+	mkenvelopes(get_visible_freq_range());
+}
+
+function demodulator_analog_replace(subtype)
+{ //this function should only exist until the multi-demodulator capability is added	
+	var temp_offset=0;
+	if(demodulators.length) 
+	{
+		temp_offset=demodulators[0].offset_frequency;
+		demodulator_remove(0);
+	}
+	demodulator_add(new demodulator_default_analog(temp_offset,subtype));
+}
+
+function demodulator_set_offset_frequency(which,to_what)
+{
+	if(to_what>bandwidth/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<demodulators.length;i++) event_handled|=demodulators[i].envelope.drag_start(evt.pageX,scale_canvas_drag_params.key_modifiers);
+		scale_canvas.style.cursor="move";
+	}
+	else if(scale_canvas_drag_params.drag)
+	{
+		//call the drag_move for all demodulators (and they will decide if they're dragged)
+		for (var i=0;i<demodulators.length;i++) event_handled|=demodulators[i].envelope.drag_move(evt.pageX);
+		if (!event_handled) demodulator_set_offset_frequency(0,scale_offset_freq_from_px(evt.pageX));
+	}
+	
+}
+
+function scale_canvas_end_drag(x)
+{
+	canvas_container.style.cursor="default";
+	scale_canvas_drag_params.drag=false;
+	scale_canvas_drag_params.mouse_down=false;
+	var event_handled=false;
+	for (var i=0;i<demodulators.length;i++) event_handled|=demodulators[i].envelope.drag_end(x);
+	//console.log(event_handled);
+	if (!event_handled) demodulator_set_offset_frequency(0,scale_offset_freq_from_px(x));
+}
+
+function scale_canvas_mouseup(evt)
+{
+	scale_canvas_end_drag(evt.pageX);
+}
+
+function scale_px_from_freq(f,range) { return Math.round(((f-range.start)/range.bw)*canvas_container.clientWidth); }
+
+function get_visible_freq_range()
+{
+	out={};
+	fcalc=function(x) { return Math.round(((-zoom_offset_px+x)/canvases[0].clientWidth)*bandwidth)+(center_freq-bandwidth/2); }
+	out.start=fcalc(0);
+	out.center=fcalc(canvas_container.clientWidth/2);
+	out.end=fcalc(canvas_container.clientWidth);
+	out.bw=out.end-out.start;
+	out.hps=out.bw/canvas_container.clientWidth;
+	return out;
+}
+
+var scale_markers_levels=[
+	{
+		"large_marker_per_hz":10000000, //large
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":0
+	},
+	{
+		"large_marker_per_hz":5000000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":0
+	},
+	{
+		"large_marker_per_hz":1000000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":0
+	},
+	{
+		"large_marker_per_hz":500000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":1
+	},
+	{
+		"large_marker_per_hz":100000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":1
+	},
+	{
+		"large_marker_per_hz":50000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":2
+	},
+	{
+		"large_marker_per_hz":10000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":2
+	},
+	{
+		"large_marker_per_hz":5000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":3
+	},
+	{
+		"large_marker_per_hz":1000,
+		"estimated_text_width":70,
+		"format":"{x} MHz",
+		"pre_divide":1000000,
+		"decimals":1
+	}
+];
+var scale_min_space_bw_texts=50;
+var scale_min_space_bw_small_markers=7;
+
+function get_scale_mark_spacing(range)
+{
+	out={};
+	fcalc=function(freq) 
+	{ 
+		out.numlarge=(range.bw/freq);
+		out.large=canvas_container.clientWidth/out.numlarge; 	//distance between large markers (these have text)
+		out.ratio=5; 														//(ratio-1) small markers exist per large marker
+		out.small=out.large/out.ratio; 								//distance between small markers
+		if(out.small<scale_min_space_bw_small_markers) return false; 
+		if(out.small/2>=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; }
+		out.smallbw=freq/out.ratio;
+		return true;
+	}
+	for(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;
+	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)
+			{ //if this is the first overall marker when zoomed out
+				if(x<text_measured.width/2)
+				{ //and if it would be clipped off the screen
+					if(scale_px_from_freq(marker_hz+spacing.smallbw*spacing.ratio,range)-text_measured.width>=scale_min_space_bw_texts)
+					{ //and if we have enough space to draw it correctly without clipping
+						scale_ctx.textAlign = "left";
+						scale_ctx.fillText(text_to_draw, 0, 22+10); 
+					}
+				}
+			}
+			else if(zoom_level==0&&range.end-spacing.smallbw*spacing.ratio<marker_hz)  
+			{ //if this is the last overall marker when zoomed out
+				if(x>window.innerWidth-text_measured.width/2) 
+				{ //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, 22+10); 
+					}	
+				}		
+			}
+			else scale_ctx.fillText(text_to_draw, x, 22+10); //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/2<window.innerWidth) scale_ctx.fillText(text_to_draw, x, 22+10);
+	}
+}
+
+function resize_scale()
+{
+	scale_ctx.canvas.width  = window.innerWidth;
+	scale_ctx.canvas.height = 47;
+	mkscale();
+}
+
+function canvas_mouseover(evt)
+{
+	if(!waterfall_setup_done) return;
+	//e("webrx-freq-show").style.visibility="visible";	
+}
+
+function canvas_mouseout(evt)
+{
+	if(!waterfall_setup_done) return;
+	//e("webrx-freq-show").style.visibility="hidden";
+}
+
+function canvas_get_freq_offset(relativeX)
+{
+	rel=(relativeX/canvases[0].clientWidth);
+	return Math.round((bandwidth*rel)-(bandwidth/2));
+}
+
+function canvas_get_frequency(relativeX)
+{
+	return center_freq+canvas_get_freq_offset(relativeX);
+}
+
+/*function canvas_format_frequency(relativeX)
+{
+	return (canvas_get_frequency(relativeX)/1e6).toFixed(3)+" MHz";
+}*/
+
+function format_frequency(format, freq_hz, pre_divide, decimals)
+{
+	out=format.replace("{x}",(freq_hz/pre_divide).toFixed(decimals));
+	at=out.indexOf(".")+4;
+	while(decimals>3)
+	{
+		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());
+		//e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4);
+		 demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX));		
+	}
+	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;
+	var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX;
+	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;
+
+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;i++)
+		zoom_levels.push(1+(maxc-1)*(i/(zoom_levels_count-1)));
+}
+
+function zoom_step(out, where, onscreen)
+{
+	if((out&&zoom_level==0)||(!out&&zoom_level>=zoom_levels_count-1)) return;
+	
+	if(out) --zoom_level;
+	else ++zoom_level;
+	zoom_center_rel=canvas_get_freq_offset(where);
+	//console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString());
+	zoom_center_where=onscreen;
+	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_px<winsize-canvases_new_width) 
+		zoom_offset_px=winsize-canvases_new_width;
+	//console.log("zoom_calc || zopx:"+zoom_offset_px.toString()+ " maxoff:"+(winsize-canvases_new_width).toString()+" relval:"+(0.5+zoom_center_rel/bandwidth).toString() );
+}
+
+function resize_waterfall_container(check_init)
+{
+	if(check_init&&!waterfall_setup_done) return;
+	canvas_container.style.height=(window.innerHeight-e("webrx-top-container").clientHeight-e("openwebrx-scale-container").clientHeight).toString()+"px";
+}
+
+
+function on_ws_recv(evt)
+{
+	/*var f = new FileReader();
+   f.onload = function(e) {
+   	//alert(e.target.result);
+	};
+	f.readAsText(evt.data);*/
+   if(!(evt.data instanceof ArrayBuffer))
+   { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; }
+	stringData=arrayBufferToString(evt.data);
+	firstChars=stringData.substring(0,3);
+	//alert(firstChars);
+	if(firstChars=="CLI" && stringData.substring(0,16)=="CLIENT DE SERVER")
+		divlog("Acknowledged WebSocket connection: "+stringData);
+	if(firstChars=="AUD")
+	{
+		audio_received[audio_received.length] = new Int16Array(evt.data,4);
+		var audio_recv_len=audio_received[audio_received.length-1].length;
+		//console.log("on_ws_recv() :: recv: "+audio_recv_len.toString());
+		audio_buffer_current_size_debug+=audio_recv_len;
+		audio_buffer_current_size+=audio_recv_len;
+		if(audio_initialized==0 && audio_received.length>10) audio_init()
+	}
+	else if(firstChars=="FFT")
+	{
+		//alert("Yupee! Doing FFT");
+		var floatArray = new Float32Array(evt.data,4);
+		waterfall_add_queue(floatArray);
+	} else if(firstChars=="MSG")
+	{
+		/*try
+		{*/
+			params=stringData.substring(4).split(" ");
+			for(i=0;i<params.length;i++)
+			{
+				param=params[i].split("=");
+				switch(param[0])
+				{
+					case "setup":
+						waterfall_init();
+						break;					
+					case "bandwidth":
+						bandwidth=parseInt(param[1])
+						break;		
+					case "center_freq":
+						center_freq=parseInt(param[1])
+						break;
+					case "fft_size":
+						fft_size=parseInt(param[1])
+						break;
+					case "fft_fps":
+						fft_fps=parseInt(param[1])
+						break;
+
+				}
+			}
+		/*}
+		catch(err)
+		{
+			divlog("Received invalid message over WebSocket.");
+		}*/
+	}
+
+}
+
+function add_problem(what)
+{
+	problems_span=e("openwebrx-problems");
+	for(var i=0;i<problems_span.children.length;i++) if(problems_span.children[i].innerHTML==what) return;
+	new_span = document.createElement("span");
+	new_span.innerHTML=what;
+	problems_span.appendChild(new_span);
+	window.setTimeout(function(ps,ns) {  ps.removeChild(ns); }, 1000,problems_span,new_span);
+}
+
+function waterfall_add_queue(what)
+{
+	waterfall_queue.push(what);
+}
+
+function waterfall_dequeue()
+{
+	if(waterfall_queue.length) waterfall_add(waterfall_queue.shift());
+	if(waterfall_queue.length>fft_fps/2) //in case of emergency 
+	{
+		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);
+}
+
+function divlog(what, is_error)
+{
+	if(typeof is_error !== undefined && is_error == 1) what="<span class=\"webrx-error\">"+what+"</span>";
+	e("openwebrx-debugdiv").innerHTML+=what+"<br />";
+}
+
+var audio_context;
+var audio_initialized=0;
+
+var audio_received = Array();
+var audio_buffer_index = 0;
+var audio_resampler;
+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 = 8192;//2048 was choppy
+var audio_buffer_maximal_length_sec=2; //actual number of samples are calculated from sample rate
+var audio_flush_interval_ms=250; //the interval in which audio_flush() is called
+
+function audio_onprocess(e) 
+{
+	//https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js
+	if(audio_received.length==0) 
+	{ add_problem("audio underrun"); return; }
+	output = e.outputBuffer.getChannelData(0);
+	int_buffer = audio_received[0];
+	read_remain = audio_buffer_size;
+	//audio_buffer_maximal_length=120;
+
+	obi=0; //output buffer index
+	debug_str=""
+	while(1)	
+	{
+		if(int_buffer.length-audio_buffer_index>read_remain)
+		{
+			for (i=audio_buffer_index; i<audio_buffer_index+read_remain; i++)
+				output[obi++] = int_buffer[i]/32768;
+			//debug_str+="added whole ibl="+int_buffer.length.toString()+" abi="+audio_buffer_index.toString()+" "+(int_buffer.length-audio_buffer_index).toString()+">"+read_remain.toString()+" obi="+obi.toString()+"\n";
+			audio_buffer_index+=read_remain;
+			break;
+		}
+		else
+		{	
+			for (i=audio_buffer_index; i<int_buffer.length; i++)
+				output[obi++] = int_buffer[i]/32768;
+			read_remain-=(int_buffer.length-audio_buffer_index);
+			audio_buffer_current_size-=audio_received[0].length;
+			/*if (audio_received.length>audio_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()
+{
+	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 audio_init()
+{
+	//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
+	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);
+	}
+	audio_node = audio_context.createJavaScriptNode(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
+	window.setInterval(audio_flush,audio_flush_interval_ms);
+	divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString());
+	/*audio_source=audio_context.createBufferSource();
+   audio_buffer = audio_context.createBuffer(xhr.response, false);
+	audio_source.buffer = buffer;
+	audio_source.noteOn(0);*/
+	demodulator_analog_replace('nfm'); //needs audio_context.sampleRate to exist
+}
+
+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(event.toString(),1);
+}
+
+function open_websocket()
+{
+	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;
+}
+
+//var color_scale=[0xFFFFFFFF, 0x000000FF];
+//var color_scale=[0x000000FF, 0x000000FF, 0x3a0090ff, 0x10c400ff, 0xffef00ff, 0xff5656ff];
+//var color_scale=[0x000000FF, 0x000000FF, 0x534b37ff, 0xcedffaff, 0x8899a9ff,  0xfff775ff, 0xff8a8aff, 0xb20000ff];
+
+//var color_scale=[ 0x000000FF, 0xff5656ff, 0xffffffff];
+
+//2014-04-22
+//var color_scale=[0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff,  0xfff775ff, 0xff8a8aff, 0xb20000ff];
+
+//Nona:
+var color_scale=[0x000000ff,0x000000ff, 0xffffffff, 0x0000ffff, 0x00ffffff];
+
+function waterfall_mkcolor(db_value)
+{
+	min_value=-100; //in dB
+	max_value=10
+	if(db_value<min_value) db_value=min_value
+	if(db_value>max_value) db_value=max_value
+	full_scale=max_value-min_value;
+	relative_value=db_value-min_value;
+	value_percent=relative_value/full_scale;
+	percent_for_one_color=1/(color_scale.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(color_scale[index+1],color_scale[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("mousewheel",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("mousewheel",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;i<w;i++) scaled[i]=-100;
+	}
+	else
+	{
+		if ((to-from)==w)
+		{
+			scaled=data;
+		}
+		else if ((to-from)<w)
+		{	//make line bigger
+			pixel_per_point=w/(to-from);
+			scaled=Array();
+			j=0;
+			remain=pixel_per_point;
+			for(i=0; i<w; i++)
+			{
+				//thiscolor=data[j]*(remain-floor(remain))+data[j+1]*(1-(remain-floor(remain)))
+				//nextcolor=data[j+1]*(remain-floor(remain))+data[j+2]*(1-(remain-floor(remain)))
+				if(remain>1)
+				{
+					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; i<to; i++)
+			{
+				if(remain>1)
+				{
+					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<w;x++)
+	{
+		color=waterfall_mkcolor(scaled[x]);
+		for(i=0;i<4;i++)
+			waterfall_image.data[base+x*4+i] = ((color>>>0)>>((3-i)*8))&0xff;
+	}*/
+
+	//Add line to waterfall image			
+	oneline_image = canvas_context.createImageData(w,1);
+	for(x=0;x<w;x++)
+	{
+		color=waterfall_mkcolor(data[x]);
+		for(i=0;i<4;i++)
+			oneline_image.data[x*4+i] = ((color>>>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; y<h-1; y++)
+	{
+		for(i=0; i<w*4; i++)
+			waterfall_image.data[y*w*4+i] = waterfall_image.data[(y+1)*w*4+i];
+	}
+}*/
+
+function check_top_bar_congestion()
+{
+	var wt=e("webrx-rx-title");
+	var tl=e("webrx-ha5kfu-top-logo");
+	if(wt.offsetLeft+wt.offsetWidth>tl.offsetLeft-20) tl.style.display="none";
+	else tl.style.display="block";
+}
+
+function webrx_resize() 
+{
+	resize_canvases();
+	resize_waterfall_container(true);
+	resize_scale();
+	check_top_bar_congestion();
+}
+
+function webrx_init()
+{
+	init_rx_photo();
+	open_websocket();
+	place_panels();
+	window.setInterval(debug_audio,1000);
+}
+
+/*
+window.setInterval(function(){ 
+	sum=0;
+	for(i=0;i<audio_received.length;i++)
+		sum+=audio_received[i].length;
+	divlog("audio buffer bytes: "+sum);
+}, 2000);*/
+
+/*function email(what)
+{
+	//| http://stackoverflow.com/questions/617647/where-is-my-one-line-implementation-of-rot13-in-javascript-going-wrong
+	what=what.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(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); }
+
+function debug_audio()
+{
+
+	e("openwebrx-audio-sps").innerHTML=audio_buffer_current_size_debug.toString();
+	audio_buffer_current_size_debug=0;
+}
+
+// ========================================================
+// =======================  PANELS  =======================
+// ========================================================
+
+panel_margin=10;
+
+function pop_bottommost_panel(from)
+{
+	min_order=parseInt(from[0].dataset.panelOrder);
+	min_index=0;
+	for(i=0;i<from.length;i++)	
+	{
+		actual_order=parseInt(from[i].dataset.panelOrder);
+		if(actual_order<min_order) 
+		{
+			min_index=i;
+			min_order=actual_order;
+		}
+	}
+	to_return=from[min_index];
+	from.splice(min_index,1);
+	return to_return;
+}
+
+function place_panels()
+{
+	var left_col=[];
+	var right_col=[];
+	var plist=e("openwebrx-panels-container").children;
+	for(i=0;i<plist.length;i++)
+	{
+		c=plist[i];
+		if(c.className=="openwebrx-panel")
+		{
+			newSize=c.dataset.panelSize.split(",");
+			if (c.dataset.panelPos=="left") { left_col.push(c); }
+			else if(c.dataset.panelPos=="right") { right_col.push(c); }
+			c.style.width=newSize[0]+"px";
+			c.style.height=newSize[1]+"px";
+			c.style.margin=panel_margin.toString()+"px";
+			c.openwebrxPanelWidth=parseInt(newSize[0]);			
+			c.openwebrxPanelHeight=parseInt(newSize[1]);
+		}
+	}
+	y=0;
+	while(left_col.length>0)
+	{
+		p=pop_bottommost_panel(left_col);
+		p.style.left="0px";
+		p.style.bottom=y.toString()+"px";
+		p.style.visibility="visible";
+		y+=p.openwebrxPanelHeight+3*panel_margin;
+	}
+	y=0;
+	while(right_col.length>0)
+	{
+		p=pop_bottommost_panel(right_col);
+		p.style.right="10px";
+		p.style.bottom=y.toString()+"px";
+		p.style.visibility="visible";
+		y+=p.openwebrxPanelHeight+3*panel_margin;
+	}
+}
+
diff --git a/htdocs/upgrade.html b/htdocs/upgrade.html
new file mode 100644
index 0000000000000000000000000000000000000000..b9a498d7cfd30fef532eb899b7812824c8ecfc53
--- /dev/null
+++ b/htdocs/upgrade.html
@@ -0,0 +1,93 @@
+<html>
+<!--
+OpenWebRX (c) Copyright 2013 Andras Retzler <ha7ilm@sdr.hu>
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX. If not, see <http://www.gnu.org/licenses/>.
+-->
+<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<style>
+html, body
+{
+	font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
+	width: 100%;
+	text-align: center;
+	margin: 0;
+	padding: 0;
+}
+img.logo
+{ 
+	margin-top: 120px;
+}
+div.frame
+{
+	text-align: left;
+	margin:0px auto;
+	width: 800px;
+}
+
+div.panel
+{
+	text-align: center;
+	background-color:#777777; 
+	border-radius: 15px;
+	padding: 12px;
+	font-weight: bold;
+	color: White;
+	font-size: 13pt;
+	/*text-shadow: 1px 1px 4px #444;*/
+	font-family: sans;
+}
+
+div.alt
+{
+	font-size: 10pt;
+	padding-top: 10px;
+}
+
+
+body div a
+{
+	color: #5ca8ff;
+	text-shadow: none;
+}
+
+span.browser
+{
+}
+
+</style>
+<script>
+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); }
+</script>
+
+</head>
+<body>
+
+<div class="frame">
+	<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
+	<div class="panel">
+		Only the latest <span class="browser">Google Chrome</span> browser is supported at the moment.<br/>
+		Please <a href="http://chrome.google.com/">download and install Google Chrome.</a><br />
+		<div class="alt">
+			Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected. <br />
+			<a href="/?unsupported">Click here</a> if you still want to try OpenWebRX.</a>
+		</div>
+	</div>
+</div>
+</body>
+</html>
+
diff --git a/openwebrx.py b/openwebrx.py
new file mode 100755
index 0000000000000000000000000000000000000000..c26cc46df2222b199b1d176781741fdee5e4e993
--- /dev/null
+++ b/openwebrx.py
@@ -0,0 +1,378 @@
+
+"""
+OpenWebRX: open-source web based SDR for everyone!
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX.  If not, see <http://www.gnu.org/licenses/>.
+
+Authors:
+    Andras Retzler, HA7ILM <randras@sdr.hu>
+
+"""
+
+# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python
+# some ideas are used from the artice above
+
+
+import os
+import code
+import importlib
+import plugins
+import plugins.dsp
+import thread
+import time
+import subprocess
+import os 
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+from SocketServer import ThreadingMixIn
+import fcntl
+import time
+import md5
+import random
+import threading
+import dl
+import sys
+import traceback
+from collections import namedtuple
+import Queue
+import ctypes
+
+#import rtl_mus
+import rxws
+import uuid
+import config_webrx as cfg
+
+def import_all_plugins(directory):
+	for subdir in os.listdir(directory):
+		if os.path.isdir(directory+subdir) and not subdir[0]=="_":
+			exact_path=directory+subdir+"/plugin.py"
+			if os.path.isfile(exact_path):
+				importname=(directory+subdir+"/plugin").replace("/",".")
+				print "[openwebrx-import] Found plugin:",importname
+				importlib.import_module(importname)
+
+class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer):
+    pass 
+
+def main():
+	global clients
+	global clients_mutex
+
+	print
+	print "OpenWebRX - Open Source Web Based SDR for Everyone  | for license see LICENSE file in the package"
+	print "_________________________________________________________________________________________________"
+	print 
+	print "Author contact info:    Andras Retzler, HA7ILM <randras@sdr.hu>"
+	print 
+
+	#Load plugins
+	import_all_plugins("plugins/dsp/")
+
+	#Change process name to "openwebrx" (to be seen in ps)
+	try:
+		for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
+			if os.path.exists(libcpath):
+				libc = dl.open(libcpath)
+				libc.call("prctl", 15, "openwebrx", 0, 0, 0)
+				break
+	except:
+		pass
+
+	#Start rtl thread
+	if cfg.start_rtl_thread:
+		rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
+		rtl_thread.start()
+		print "[openwebrx-main] Started rtl thread: "+cfg.start_rtl_command
+
+	#Run rtl_mus.py in a different OS thread
+	rtl_mus_thread=threading.Thread(target = lambda:subprocess.Popen("python rtl_mus.py config_rtl", shell=True), args=())
+	rtl_mus_thread.start() # The new feature in GNU Radio 3.7: top_block() locks up ALL python threads until it gets the TCP connection.
+	print "[openwebrx-main] Started rtl_mus"
+	time.sleep(1) #wait until it really starts	
+
+	#Initialize clients
+	clients=[]
+	clients_mutex=threading.Lock()
+
+	#Start spectrum thread
+	print "[openwebrx-main] Starting spectrum thread."
+	spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ())
+	spectrum_thread.start()
+	
+	#threading.Thread(target = measure_thread_function, args = ()).start()
+	
+	#Start HTTP thread
+	httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler)
+	print('[openwebrx-main] Starting HTTP server.')
+	httpd.serve_forever()
+
+
+# This is a debug function below:
+measure_value=0
+def measure_thread_function():
+	global measure_value
+	while True:	
+		print "[openwebrx-measure] value is",measure_value
+		measure_value=0
+		time.sleep(1)
+
+
+def spectrum_thread_function():
+	global clients_mutex
+	global clients
+	dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin()
+	dsp.set_demodulator("fft")
+	dsp.set_samp_rate(cfg.samp_rate)
+	dsp.set_fft_size(cfg.fft_size)
+	dsp.set_fft_fps(cfg.fft_fps)
+	sleep_sec=0.87/cfg.fft_fps
+	print "[openwebrx-spectrum] Spectrum thread initialized successfully. Thread id:", ctypes.CDLL('/lib/i386-linux-gnu/libc.so.6').syscall(224)
+	dsp.start()
+	print "[openwebrx-spectrum] Spectrum thread started." 
+	while True:
+		data=dsp.read(cfg.fft_size*4)
+		#print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
+		clients_mutex.acquire()
+		for i in range(0,len(clients)):
+			if (clients[i].ws_started):
+				if clients[i].spectrum_queue.full():
+					close_client(i, False)
+				else:
+					clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
+		clients_mutex.release()
+	
+def get_client_by_id(client_id, use_mutex=True):
+	global clients_mutex
+	global clients
+	output=-1
+	if use_mutex: clients_mutex.acquire()
+	for i in range(0,len(clients)):
+		if(clients[i].id==client_id):
+			output=i
+			break
+	if use_mutex: clients_mutex.release()
+	if output==-1:
+		raise ClientNotFoundException
+	else:
+		return output
+
+def log_client(client, what):
+	print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
+
+def cleanup_clients():
+	# if client doesn't open websocket for too long time, we drop it
+	global clients_mutex
+	global clients
+	clients_mutex.acquire()
+	correction=0
+	for i in range(0,len(clients)):
+		i-=correction
+		#print "cleanup_clients:: len(clients)=", len(clients), "i=", i
+		if (not clients[i].ws_started) and (time.time()-clients[i].gen_time)>180:
+			close_client(i, False)
+			correction+=1
+	clients_mutex.release()
+
+def generate_client_id(ip):
+	#add a client
+	global clients
+	global clients_mutex
+	new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip")	
+	new_client.id=md5.md5(str(random.random())).hexdigest()
+	new_client.gen_time=time.time()
+	new_client.ws_started=False # to check whether client has ever tried to open the websocket
+	new_client.spectrum_queue=Queue.Queue(1000)
+	new_client.ip=ip
+	clients_mutex.acquire()
+	clients.append(new_client)
+	log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
+	clients_mutex.release()
+	cleanup_clients()
+	return new_client.id
+
+def close_client(i, use_mutex=True):
+	global clients_mutex
+	global clients
+	log_client(clients[i],"client being closed.")
+	if use_mutex: clients_mutex.acquire()
+	del clients[i]
+	if use_mutex: clients_mutex.release()
+	
+class WebRXHandler(BaseHTTPRequestHandler):    
+	def proc_read_thread():
+		pass
+
+	def do_GET(self):
+		global dsp_plugin
+		rootdir = 'htdocs' 
+		self.path=self.path.replace("..","")
+		path_temp_parts=self.path.split("?")
+		self.path=path_temp_parts[0]
+		request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else "" 
+		try:
+			if self.path=="/":
+				self.path="/index.wrx"
+			# there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python
+			if self.path[:4]=="/ws/":
+				try:
+					# ========= WebSocket handshake  =========
+					try:				
+						rxws.handshake(self)
+						clients_mutex.acquire()				
+						client_i=get_client_by_id(self.path[4:], False)
+						myclient=clients[client_i]
+						clients_mutex.release()
+					except rxws.WebSocketException:
+						self.send_error(400, 'Bad request.')
+						return
+					except ClientNotFoundException:
+						self.send_error(400, 'Bad request.')
+						return
+
+					# ========= Client handshake =========
+					rxws.send(self, "CLIENT DE SERVER openwebrx.py")
+					client_ans=rxws.recv(self, True)
+					if client_ans[:16]!="SERVER DE CLIENT":
+						rxws.send("ERR Bad answer.")
+						return
+					myclient.ws_started=True
+					#send default parameters
+					rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} setup".format(str(cfg.center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps))
+
+					# ========= Initialize DSP =========
+					dsp=getattr(plugins.dsp,cfg.dsp_plugin).plugin.dsp_plugin()
+					dsp.set_samp_rate(cfg.samp_rate)
+					dsp.set_demodulator("nfm")
+					dsp.set_offset_freq(0)
+					dsp.set_bpf(-4000,4000)
+					dsp.start()
+					
+					while True:
+						# ========= send audio =========
+						temp_audio_data=dsp.read(1024*8)
+						rxws.send(self, temp_audio_data, "AUD ")
+
+						# ========= send spectrum =========
+						while not myclient.spectrum_queue.empty():
+							spectrum_data=myclient.spectrum_queue.get()
+							spectrum_data_mid=len(spectrum_data[0])/2
+							rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ") 
+							# (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it)
+
+						# ========= process commands =========
+						while True:
+							rdata=rxws.recv(self, False)
+							if not rdata: break
+							#try:
+							elif rdata[:3]=="SET":
+								print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata)
+								pairs=rdata[4:].split(" ")
+								bpf_set=False
+								new_bpf=dsp.get_bpf()
+								filter_limit=dsp.get_output_rate()/2
+								for pair in pairs:
+									param_name, param_value = pair.split("=")
+									if param_name == "low_cut" and -filter_limit <= float(param_value) <= filter_limit:
+										bpf_set=True
+										new_bpf[0]=param_value
+									elif param_name == "high_cut" and -filter_limit <= float(param_value) <= filter_limit:
+										bpf_set=True
+										new_bpf[1]=param_value
+									elif param_name == "offset_freq" and -cfg.samp_rate/2 <= float(param_value) <= cfg.samp_rate/2:
+										dsp.set_offset_freq(param_value)
+									elif param_name=="mod":
+										dsp.stop()
+										dsp.set_demodulator(param_value)
+										dsp.start()
+									else:
+										print "[openwebrx-httpd:ws] invalid parameter"
+								if bpf_set:
+									dsp.set_bpf(*new_bpf)
+								#code.interact(local=locals())
+				except:
+					print "[openwebrx-httpd] exception happened at all"
+					exc_type, exc_value, exc_traceback = sys.exc_info()
+					if exc_value[0]==32: #"broken pipe", client disconnected
+						pass
+					elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected					
+						pass
+					else:	
+						print "[openwebrx-httpd] error: ",exc_type,exc_value
+						traceback.print_tb(exc_traceback)
+				#delete disconnected client
+				try:
+					dsp.stop()
+					del dsp
+				except:
+					pass
+				clients_mutex.acquire()
+				id_to_close=get_client_by_id(myclient.id,False)
+				close_client(id_to_close,False)
+				clients_mutex.release()
+				return
+			else:
+				f=open(rootdir+self.path)
+				data=f.read()
+				extension=self.path[(len(self.path)-4):len(self.path)]
+				extension=extension[2:] if extension[1]=='.' else extension[1:]
+				if extension == "wrx" and ((self.headers['user-agent'].count("Chrome")==0 and self.headers['user-agent'].count("Firefox")==0) if 'user-agent' in self.headers.keys() else True) and (not request_param.count("unsupported")):
+					self.send_response(302)
+					self.send_header('Content-type','text/html')
+					self.send_header("Location", "http://{0}:{1}/upgrade.html".format(cfg.server_hostname,cfg.web_port))
+					self.end_headers()
+					self.wfile.write("<html><body><h1>Object moved</h1>Please <a href=\"/upgrade.html\">click here</a> to continue.</body></html>")
+					return
+				self.send_response(200)
+				if(("wrx","html","htm").count(extension)):
+					self.send_header('Content-type','text/html')
+				elif(extension=="js"):
+					self.send_header('Content-type','text/javascript')
+				elif(extension=="css"):
+					self.send_header('Content-type','text/css')
+				self.end_headers()
+				if extension == "wrx":
+					replace_dictionary=(
+						("%[RX_PHOTO_DESC]",cfg.photo_desc),
+						("%[CLIENT_ID]",generate_client_id(self.client_address[0])),
+						("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"),
+						("%[RX_TITLE]",cfg.receiver_name),
+						("%[RX_LOC]",cfg.receiver_location),
+						("%[RX_QRA]",cfg.receiver_qra),
+						("%[RX_ASL]",str(cfg.receiver_asl)),
+						("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])),
+						("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title),
+						("%[RX_ADMIN]",cfg.receiver_admin),
+						("%[RX_ANT]",cfg.receiver_ant),
+						("%[RX_DEVICE]",cfg.receiver_device)
+					)
+					for rule in replace_dictionary:
+						while data.find(rule[0])!=-1:
+							data=data.replace(rule[0],rule[1])
+				self.wfile.write(data)
+				f.close()
+			return
+		except IOError:
+			self.send_error(404, 'Invalid path.')
+		except:
+			exc_type, exc_value, exc_traceback = sys.exc_info()
+			print "[openwebrx-httpd] exception happened (outside):", exc_type, exc_value
+			traceback.print_tb(exc_traceback)
+
+class ClientNotFoundException(Exception):
+	pass
+
+if __name__=="__main__":
+	main()
+
diff --git a/plugins/__init__.py b/plugins/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/plugins/__init__.pyc b/plugins/__init__.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d84020a8aafa5ed8b81c5ef7ca1a2c3e5cb066c6
Binary files /dev/null and b/plugins/__init__.pyc differ
diff --git a/plugins/dsp/__init__.py b/plugins/dsp/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/plugins/dsp/__init__.pyc b/plugins/dsp/__init__.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7dc2093abf01c40966a1362737984ba224f467ff
Binary files /dev/null and b/plugins/dsp/__init__.pyc differ
diff --git a/plugins/dsp/csdr/__init__.py b/plugins/dsp/csdr/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/plugins/dsp/csdr/__init__.pyc b/plugins/dsp/csdr/__init__.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7f0bd13c6f8c07fc9d65ee9403809c44829c854d
Binary files /dev/null and b/plugins/dsp/csdr/__init__.pyc differ
diff --git a/plugins/dsp/csdr/plugin.py b/plugins/dsp/csdr/plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..511c9629e5ee19a299bfbd644ffde781ca0150e1
--- /dev/null
+++ b/plugins/dsp/csdr/plugin.py
@@ -0,0 +1,135 @@
+import subprocess
+import time
+import os
+import code
+
+class dsp_plugin:
+
+	def __init__(self):
+		self.samp_rate = 250000
+		self.output_rate = 44100 #this is default, and cannot be set at the moment
+		self.fft_size = 1024
+		self.fft_fps = 5
+		self.offset_freq = 0
+		self.low_cut = -4000
+		self.high_cut = 4000
+		self.bpf_transition_bw = 300 #Hz, and this is a constant
+		self.running = False
+		chain_begin="nc localhost 4951 | csdr convert_u8_f | csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} 0.005 HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | "
+		self.chains = {
+			"nfm" :  chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 48000 | csdr fastagc_ff | csdr convert_f_i16",
+			"am" :  chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_i16",
+			"ssb" :  chain_begin + "csdr realpart_cf | csdr fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_i16",
+			"fft" : "nc -vv localhost 4951 | csdr convert_u8_f | csdr fft_cc {fft_size} {fft_block_size} | csdr logpower_cf -70"
+			}
+		self.demodulator = "nfm"
+		self.name = "csdr"
+		try:	
+			subprocess.Popen("nc",stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+		except:
+			print "[openwebrx-plugin:csdr] error: netcat not found, please install netcat!"
+
+	def set_samp_rate(self,samp_rate):
+		#to change this, restart is required
+		self.samp_rate=samp_rate
+		self.decimation=1
+		while self.samp_rate/(self.decimation+1)>self.output_rate:
+			self.decimation+=1
+		self.last_decimation=float(self.if_samp_rate())/self.output_rate
+
+	def if_samp_rate(self):
+		return self.samp_rate/self.decimation
+
+	def get_name(self):
+		return self.name
+	
+	def get_output_rate(self):
+		return self.output_rate
+
+
+	def set_demodulator(self,demodulator):
+		#to change this, restart is required
+		self.demodulator=demodulator
+
+	def set_fft_size(self,fft_size):
+		#to change this, restart is required
+		self.fft_size=fft_size
+
+	def set_fft_fps(self,fft_fps):
+		#to change this, restart is required
+		self.fft_fps=fft_fps
+	
+	def fft_block_size(self):
+		return self.samp_rate/self.fft_fps
+
+	def set_offset_freq(self,offset_freq):
+		self.offset_freq=offset_freq
+		if self.running: 
+			self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
+			self.shift_pipe_file.flush()
+	
+	def set_bpf(self,low_cut,high_cut):
+		self.low_cut=low_cut
+		self.high_cut=high_cut
+		if self.running: 
+			self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
+			self.bpf_pipe_file.flush()
+		
+	def get_bpf(self):
+		return [self.low_cut, self.high_cut]
+
+	def mkfifo(self,path):
+		try:
+			os.unlink(path)
+		except:
+			pass
+		os.mkfifo(path)	
+
+	def start(self):
+		command_base=self.chains[self.demodulator]
+		
+		#create control pipes for csdr
+		pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
+		self.bpf_pipe = self.shift_pipe = None
+		if "{bpf_pipe}" in command_base:
+			self.bpf_pipe=pipe_base_path+"bpf"
+			self.mkfifo(self.bpf_pipe)
+		if "{shift_pipe}" in command_base:
+			self.shift_pipe=pipe_base_path+"shift"
+			self.mkfifo(self.shift_pipe)
+
+		#run the command
+		command=command_base.format(bpf_pipe=self.bpf_pipe,shift_pipe=self.shift_pipe,decimation=self.decimation,last_decimation=self.last_decimation,fft_size=self.fft_size,fft_block_size=self.fft_block_size(),bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate())
+		print "[openwebrx-dsp-plugin:csdr] Command =",command
+		#code.interact(local=locals())
+		self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
+		self.running = True
+
+		#open control pipes for csdr and send initialization data
+		if self.bpf_pipe != None: 
+			self.bpf_pipe_file=open(self.bpf_pipe,"w")
+			self.set_bpf(self.low_cut,self.high_cut)
+		if self.shift_pipe != None: 
+			self.shift_pipe_file=open(self.shift_pipe,"w")
+			self.set_offset_freq(self.offset_freq)
+
+	def read(self,size):
+		return self.process.stdout.read(size)
+		
+	def stop(self):
+		if(self.process!=None):return # returns None while subprocess is running
+		while(self.process.poll()==None):
+			self.process.kill()
+			time.sleep(0.1)
+		os.unlink(self.bpf_pipe)
+		os.unlink(self.shift_pipe)
+		self.running = False
+
+	def restart(self):
+		self.stop()
+		self.start()
+
+	def __del__(self):
+		self.stop()
+		del(self.process)
+	
diff --git a/plugins/dsp/csdr/plugin.pyc b/plugins/dsp/csdr/plugin.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4e0345fa8fdcaec6b438762bdff219092d99be10
Binary files /dev/null and b/plugins/dsp/csdr/plugin.pyc differ
diff --git a/rtl_mus.py b/rtl_mus.py
new file mode 100644
index 0000000000000000000000000000000000000000..b348961b8fabb558605d7f8198b79fd6806b8415
--- /dev/null
+++ b/rtl_mus.py
@@ -0,0 +1,514 @@
+'''
+This file is part of RTL Multi-User Server, 
+	that makes multi-user access to your DVB-T dongle used as an SDR.
+Copyright (c) 2013-2014 by Andras Retzler, HA7ILM <randras@sdr.hu>
+
+RTL Multi-User Server is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RTL Multi-User Server 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with RTL Multi-User Server.  If not, see <http://www.gnu.org/licenses/>.
+
+-----
+
+2013-11?  Asyncore version
+2014-03   Fill with null on no data
+
+'''
+
+import socket 
+import sys
+import array
+import time
+import logging
+import os
+import time
+import subprocess
+import fcntl
+import thread
+import pdb
+import asyncore
+import multiprocessing
+import dl
+
+import code
+import traceback
+
+def ip_match(this,ip_ranges,for_allow):
+	if not len(ip_ranges):
+		return 1 #empty list matches all ip addresses
+	for ip_range in ip_ranges:
+		#print this[0:len(ip_range)], ip_range
+		if this[0:len(ip_range)]==ip_range:
+			return 1
+	return 0
+
+def ip_access_control(ip):
+	if(not cfg.use_ip_access_control): return 1
+	allowed=0
+	if(cfg.order_allow_deny):
+		if ip_match(ip,cfg.allowed_ip_ranges,1): allowed=1
+		if ip_match(ip,cfg.denied_ip_ranges,0): allowed=0
+	else:
+		if ip_match(ip,cfg.denied_ip_ranges,0): 
+			allowed=0
+		if ip_match(ip,cfg.allowed_ip_ranges,1): 
+			allowed=1
+	return allowed
+
+def add_data_to_clients(new_data):
+	# might be called from:
+	# -> dsp_read
+	# -> rtl_tcp_asyncore.handle_read
+	global clients
+	global clients_mutex
+	clients_mutex.acquire()
+	for client in clients:
+		#print "client %d size: %d"%(client[0].ident,client[0].waiting_data.qsize())
+		if(client[0].waiting_data.full()):
+			if cfg.cache_full_behaviour == 0:
+				log.error("client cache full, dropping samples: "+str(client[0].ident)+"@"+client[0].socket[1][0])
+				while not client[0].waiting_data.empty(): # clear queue
+					client[0].waiting_data.get(False, None)
+			elif cfg.cache_full_behaviour == 1:
+				#rather closing client:
+				log.error("client cache full, dropping client: "+str(client[0].ident)+"@"+client[0].socket[1][0])
+				client[0].close()
+			elif cfg.cache_full_behaviour == 2:
+				pass #client cache full, just not taking care
+			else: log.error("invalid value for cfg.cache_full_behaviour")
+		else:
+			client[0].waiting_data.put(new_data)
+	clients_mutex.release()
+
+def dsp_read_thread():
+	global proc
+	global dsp_data_count
+	while True:
+		try:
+			my_buffer=proc.stdout.read(1024)
+		except IOError:
+			log.error("DSP subprocess is not ready for reading.")
+			time.sleep(1)
+			continue
+		add_data_to_clients(my_buffer)
+		if cfg.debug_dsp_command:
+			dsp_data_count+=len(my_buffer)	
+
+def dsp_write_thread():
+	global proc
+	global dsp_input_queue
+	global original_data_count
+	while True:
+		try:
+			my_buffer=dsp_input_queue.get(timeout=0.3)
+		except:
+			continue
+		proc.stdin.write(my_buffer)
+		proc.stdin.flush()
+		if cfg.debug_dsp_command:
+			original_data_count+=len(my_buffer)
+
+class client_handler(asyncore.dispatcher):
+
+	def __init__(self,client_param):
+		self.client=client_param
+		self.client[0].asyncore=self
+		self.sent_dongle_id=False
+		self.last_waiting_buffer=""
+		asyncore.dispatcher.__init__(self, self.client[0].socket[0])
+
+	def handle_read(self):
+		global commands
+		new_command = self.recv(5)
+		if len(new_command)>=5:
+			if handle_command(new_command, self.client):
+				commands.put(new_command)
+
+	def handle_error(self):
+		exc_type, exc_value, exc_traceback = sys.exc_info()
+		log.info("client error: "+str(self.client[0].ident)+"@"+self.client[0].socket[1][0])
+		traceback.print_tb(exc_traceback)
+		self.close()
+
+	def handle_close(self):
+		self.client[0].close()
+		log.info("client disconnected: "+str(self.client[0].ident)+"@"+self.client[0].socket[1][0])
+
+	def writable(self):
+		#print "queryWritable",not self.client[0].waiting_data.empty()
+		return not self.client[0].waiting_data.empty()
+
+	def handle_write(self):
+		global last_waiting
+		global rtl_dongle_identifier
+		global sample_rate
+		if not self.sent_dongle_id:
+			self.send(rtl_dongle_identifier)
+			self.sent_dongle_id=True
+			return
+		#print "write2client",self.client[0].waiting_data.qsize()
+		next=self.last_waiting_buffer+self.client[0].waiting_data.get()
+		sent=asyncore.dispatcher.send(self, next)
+		self.last_waiting_buffer=next[sent:]
+
+class server_asyncore(asyncore.dispatcher):
+
+	def __init__(self):
+		asyncore.dispatcher.__init__(self)
+		self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+		self.set_reuse_addr()
+		self.bind((cfg.my_ip, cfg.my_listening_port))
+		self.listen(5)
+		log.info("Server listening on port: "+str(cfg.my_listening_port))
+
+	def handle_accept(self):
+		global max_client_id
+		global clients_mutex
+		global clients
+		my_client=[client()]
+		my_client[0].socket=self.accept()
+		if (my_client[0].socket is None): # not sure if required
+			return 
+		if (ip_access_control(my_client[0].socket[1][0])):
+			my_client[0].ident=max_client_id
+			max_client_id+=1
+			my_client[0].start_time=time.time()
+			my_client[0].waiting_data=multiprocessing.Queue(250)
+			clients_mutex.acquire()
+			clients.append(my_client)
+			clients_mutex.release()
+			handler = client_handler(my_client)
+			log.info("client accepted: "+str(len(clients)-1)+"@"+my_client[0].socket[1][0]+":"+str(my_client[0].socket[1][1])+"  users now: "+str(len(clients)))
+		else:
+			log.info("client denied: "+str(len(clients)-1)+"@"+my_client[0].socket[1][0]+":"+str(my_client[0].socket[1][1])+" blocked by ip")
+			my_client.socket.close()
+
+rtl_tcp_resetting=False #put me away
+
+def rtl_tcp_asyncore_reset(timeout):
+	global rtl_tcp_core
+	global rtl_tcp_resetting
+	if rtl_tcp_resetting: return
+	#print "rtl_tcp_asyncore_reset"
+	rtl_tcp_resetting=True
+	time.sleep(timeout)
+	try:
+		rtl_tcp_core.close()
+	except:
+		pass
+	try:
+		del rtl_tcp_core
+	except:
+		pass
+	rtl_tcp_core=rtl_tcp_asyncore()
+	#print asyncore.socket_map
+	rtl_tcp_resetting=False
+
+class rtl_tcp_asyncore(asyncore.dispatcher):
+	def __init__(self):
+		global server_missing_logged
+		asyncore.dispatcher.__init__(self)
+		self.ok=True
+		self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+		try:		
+			self.connect((cfg.rtl_tcp_host, cfg.rtl_tcp_port))
+			self.socket.settimeout(0.1)
+		except:
+			log.error("rtl_tcp connection refused. Retrying.")
+			thread.start_new_thread(rtl_tcp_asyncore_reset, (1,))
+			self.close()
+			return
+
+	def handle_error(self):
+		global server_missing_logged
+		global rtl_tcp_connected
+		rtl_tcp_connected=False
+		exc_type, exc_value, exc_traceback = sys.exc_info()
+		self.ok=False
+		server_is_missing=hasattr(exc_value,"errno") and exc_value.errno==111
+		if (not server_is_missing) or (not server_missing_logged):
+			log.error("with rtl_tcp host connection: "+str(exc_value))
+			#traceback.print_tb(exc_traceback)
+			server_missing_logged|=server_is_missing
+		try:
+			self.close()
+		except:
+			pass
+		thread.start_new_thread(rtl_tcp_asyncore_reset, (2,))
+
+	def handle_connect(self):
+		global server_missing_logged
+		global rtl_tcp_connected
+		self.socket.settimeout(0.1)
+		rtl_tcp_connected=True
+		if self.ok:
+			log.info("rtl_tcp host connection estabilished")
+			server_missing_logged=False
+
+	def handle_close(self):
+		global rtl_tcp_connected
+		global rtl_tcp_core
+		rtl_tcp_connected=False
+		log.error("rtl_tcp host connection has closed, now trying to reopen")
+		try:
+			self.close()
+		except:
+			pass
+		thread.start_new_thread(rtl_tcp_asyncore_reset, (2,))
+
+	def handle_read(self):
+		global rtl_dongle_identifier
+		global dsp_input_queue
+		global watchdog_data_count
+		if(len(rtl_dongle_identifier)==0):
+			rtl_dongle_identifier=self.recv(12)
+			return
+		new_data_buffer=self.recv(16348)
+		if cfg.watchdog_interval:
+			watchdog_data_count+=16348
+		if cfg.use_dsp_command:
+			dsp_input_queue.put(new_data_buffer)
+			#print "did put anyway"
+		else:
+			add_data_to_clients(new_data_buffer)
+
+	def writable(self):
+		#check if any new commands to write
+		global commands
+		return not commands.empty()
+
+	def handle_write(self):
+		global commands
+		while not commands.empty():
+			mcmd=commands.get()
+			self.send(mcmd)
+
+def xxd(data):
+	#diagnostic purposes only
+	output=""
+	for d in data:
+		output+=hex(ord(d))[2:].zfill(2)+" " 
+	return output
+
+def handle_command(command, client_param):
+	global sample_rate
+	client=client_param[0]
+	param=array.array("I", command[1:5])[0]
+	param=socket.ntohl(param)
+	command_id=ord(command[0])
+	client_info=str(client.ident)+"@"+client.socket[1][0]+":"+str(client.socket[1][1])
+	if(time.time()-client.start_time<cfg.client_cant_set_until and not (cfg.first_client_can_set and client.ident==0) ):
+		log.info("deny: "+client_info+" -> client can't set anything until "+str(cfg.client_cant_set_until)+" seconds")
+		return 0
+	if command_id == 1:
+		if max(map((lambda r: param>=r[0] and param<=r[1]),cfg.freq_allowed_ranges)):
+			log.debug("allow: "+client_info+" -> set freq "+str(param))
+			return 1
+		else:
+			log.debug("deny: "+client_info+" -> set freq - out of range: "+str(param))
+	elif command_id == 2:
+		log.debug("deny: "+client_info+" -> set sample rate: "+str(param))
+		sample_rate=param
+		return 0 # ordinary clients are not allowed to do this
+	elif command_id == 3:
+		log.debug("deny/allow: "+client_info+" -> set gain mode: "+str(param))
+		return cfg.allow_gain_set
+	elif command_id == 4:
+		log.debug("deny/allow: "+client_info+" -> set gain: "+str(param))
+		return cfg.allow_gain_set 
+	elif command_id == 5:
+		log.debug("deny: "+client_info+" -> set freq correction: "+str(param))
+		return 0 
+	elif command_id == 6:
+		log.debug("deny/allow: set if stage gain")
+		return cfg.allow_gain_set
+	elif command_id == 7:
+		log.debug("deny: set test mode")
+		return 0
+	elif command_id == 8:
+		log.debug("deny/allow: set agc mode")
+		return cfg.allow_gain_set
+	elif command_id == 9:
+		log.debug("deny: set direct sampling")
+		return 0
+	elif command_id == 10:
+		log.debug("deny: set offset tuning")
+		return 0
+	elif command_id == 11:
+		log.debug("deny: set rtl xtal")
+		return 0
+	elif command_id == 12:
+		log.debug("deny: set tuner xtal")
+		return 0
+	elif command_id == 13:
+		log.debug("deny/allow: set tuner gain by index")
+		return cfg.allow_gain_set
+	else:
+		log.debug("deny: "+client_info+" sent an ivalid command: "+str(param))
+	return 0
+
+def watchdog_thread():
+	global rtl_tcp_connected
+	global rtl_tcp_core	
+	global watchdog_data_count
+	global sample_rate
+	zero_buffer_size=16348
+	second_frac=10
+	zero_buffer='\x7f'*zero_buffer_size
+	watchdog_data_count=0
+	rtl_tcp_connected=False
+	null_fill=False
+	time.sleep(4) # wait before activating this thread
+	log.info("watchdog started")
+	first_start=True
+	n=0
+	while True:
+		wait_altogether=cfg.watchdog_interval if rtl_tcp_connected or first_start else cfg.reconnect_interval	
+		first_start=False
+		if null_fill:
+			log.error("watchdog: filling buffer with zeros.")	
+			while wait_altogether>0:
+				wait_altogether-=1.0/second_frac
+				for i in range(0,((2*sample_rate)/second_frac)/zero_buffer_size):	
+					add_data_to_clients(zero_buffer)
+					n+=len(zero_buffer)
+					time.sleep(0) #yield
+					if watchdog_data_count: break
+				if watchdog_data_count: break
+				time.sleep(1.0/second_frac)
+				#print "sent altogether",n
+		else:
+			time.sleep(wait_altogether)
+		null_fill=not watchdog_data_count
+		if not watchdog_data_count:
+			log.error("watchdog: restarting rtl_tcp_asyncore() now.")
+			rtl_tcp_asyncore_reset(0)
+		watchdog_data_count=0
+			
+		
+
+def dsp_debug_thread():
+	global dsp_data_count
+	global original_data_count
+	while 1:	
+		time.sleep(1)
+		print "[rtl-mus] DSP | Original data: "+str(int(original_data_count/1000))+"kB/sec | Processed data: "+str(int(dsp_data_count/1000))+"kB/sec"
+		dsp_data_count = original_data_count=0
+		
+class client:
+	ident=None #id
+	to_close=False
+	waiting_data=None
+	start_time=None	
+	socket=None
+	asyncore=None
+
+	def close(self):
+		global clients_mutex
+		global clients
+		clients_mutex.acquire()
+		for i in range(0,len(clients)):
+			if clients[i][0].ident==self.ident:
+				try:
+					self.socket.close()
+				except:
+					pass
+				try:
+					self.asyncore.close()
+					del self.asyncore
+				except:
+					pass
+				break
+		clients_mutex.release()
+
+
+def main():
+	global server_missing_logged
+	global rtl_dongle_identifier
+	global log
+	global clients
+	global clients_mutex
+	global original_data_count
+	global dsp_input_queue
+	global dsp_data_count
+	global proc
+	global commands
+	global max_client_id
+	global rtl_tcp_core
+	global sample_rate
+
+	# set up logging
+	log = logging.getLogger("rtl_mus")
+	log.setLevel(logging.DEBUG)
+	formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+	stream_handler = logging.StreamHandler()
+	stream_handler.setLevel(logging.DEBUG)
+	stream_handler.setFormatter(formatter)
+	log.addHandler(stream_handler)
+	file_handler = logging.FileHandler(cfg.log_file_path)
+	file_handler.setLevel(logging.INFO)
+	file_handler.setFormatter(formatter)
+	log.addHandler(file_handler)
+	log.info("Server is UP")
+	
+	server_missing_logged=0	# Not to flood the screen with messages related to rtl_tcp disconnect
+	rtl_dongle_identifier='' # rtl_tcp sends some identifier on dongle type and gain values in the first few bytes right after connection
+	clients=[]
+	dsp_data_count=original_data_count=0
+	commands=multiprocessing.Queue()
+	dsp_input_queue=multiprocessing.Queue()
+	clients_mutex=multiprocessing.Lock()
+	max_client_id=0
+	sample_rate=250000 # so far only watchdog thread uses it to fill buffer up with zeros on missing input
+
+	# start dsp threads
+	if cfg.use_dsp_command:
+		print "[rtl_mus] Opening DSP process..."
+		proc = subprocess.Popen (cfg.dsp_command.split(" "), stdin = subprocess.PIPE, stdout = subprocess.PIPE) #!! should fix the split :-S
+		dsp_read_thread_v=thread.start_new_thread(dsp_read_thread, ())
+		dsp_write_thread_v=thread.start_new_thread(dsp_write_thread, ())
+		if cfg.debug_dsp_command:
+			dsp_debug_thread_v=thread.start_new_thread(dsp_debug_thread,())
+
+	# start watchdog thread
+	if cfg.watchdog_interval != 0:
+		watchdog_thread_v=thread.start_new_thread(watchdog_thread,())
+
+	# start asyncores
+	rtl_tcp_core = rtl_tcp_asyncore()
+	server_core = server_asyncore()
+
+	asyncore.loop(0.1)
+
+
+if __name__=="__main__":
+	print
+	print "rtl_mus: Multi-User I/Q Data Server for RTL-SDR v0.22, made at HA5KFU Amateur Radio Club (http://ha5kfu.hu)"
+	print "    code by Andras Retzler, HA7ILM <randras@sdr.hu>"
+	print "    distributed under GNU GPL v3"
+	print 
+
+	for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
+		if os.path.exists(libcpath):
+			libc = dl.open(libcpath)
+			libc.call("prctl", 15, "rtl_mus", 0, 0, 0)
+			break
+
+	# === Load configuration script ===
+	if len(sys.argv)==1:
+		print "[rtl_mus] Warning! Configuration script not specified. I will use: \"config_rtl.py\""
+		config_script="config_rtl"
+	else:
+		config_script=sys.argv[1]
+	cfg=__import__(config_script)
+	if cfg.setuid_on_start:
+		os.setuid(cfg.uid)
+	main()
diff --git a/rxws.py b/rxws.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b42d6cabbb348c35f798a51a01e6cf01dc999f3
--- /dev/null
+++ b/rxws.py
@@ -0,0 +1,148 @@
+"""
+rxws: WebSocket methods implemented for OpenWebRX
+
+This file is part of OpenWebRX.
+
+    OpenWebRX is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenWebRX 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenWebRX.  If not, see <http://www.gnu.org/licenses/>.
+
+Authors:
+    Andras Retzler, HA7ILM <retzlerandras@gmail.com>
+
+"""
+
+import base64
+import sha
+import select
+
+class WebSocketException(Exception):
+	pass
+
+def handshake(myself):
+	my_client_id=myself.path[4:]
+	my_headers=myself.headers.items()
+	my_header_keys=map(lambda x:x[0],my_headers)
+	h_key_exists=lambda x:my_header_keys.count(x)
+	h_value=lambda x:my_headers[my_header_keys.index(x)][1]
+	#print "The Lambdas(tm)"
+	#print h_key_exists("upgrade")
+	#print h_value("upgrade")
+	#print h_key_exists("sec-websocket-key")
+	if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")):
+		raise WebSocketException
+	ws_key=h_value("sec-websocket-key")
+	ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest())
+	#A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')]
+	myself.connection.send("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n")
+
+def get_header(size):
+	#this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php
+	ws_first_byte=0b10000010 # FIN=1, OP=2
+	if(size>125):
+		ws_second_byte=126 # The following two bytes will indicate frame size
+		extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP
+	else:
+		ws_second_byte=size
+		#256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data]
+		extended_size=""
+	return chr(ws_first_byte)+chr(ws_second_byte)+extended_size
+
+def code_payload(data, masking_key=""):
+	# both encode or decode
+	if masking_key=="":
+		key = (61, 84, 35, 6)
+	else:
+		key = [ord(i) for i in masking_key]
+	encoded=""
+	for i in range(0,len(data)):
+		encoded+=chr(ord(data[i])^key[i%4])
+	return encoded
+
+def xxdg(data):
+	output=""
+	for i in range(0,len(data)/8):
+		output+=xxd(data[i:i+8])
+		if i%2: output+="\n"
+		else: output+="  "
+	return output
+		
+
+def xxd(data):
+	#diagnostic purposes only
+	output=""
+	for d in data:
+		output+=hex(ord(d))[2:].zfill(2)+" " 
+	return output
+
+def recv(myself, blocking=False, debug=False):
+	bufsize=70000
+	myself.connection.setblocking(blocking)
+	if debug: print "ws_recv begin"
+	try:
+		data=myself.connection.recv(6)
+		#print "rxws.recv bytes:",xxd(data)	
+	except:
+		if debug: print "ws_recv error"	
+		return ""
+	if debug: print "ws_recv recved"
+	if(len(data)==0): return ""
+	fin=ord(data[0])&128!=0
+	is_text_frame=ord(data[0])&15==1
+	length=ord(data[1])&0x7f
+	data+=myself.connection.recv(length)
+	#print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data)
+	has_one_byte_length=length<125
+	masked=ord(data[1])&0x80!=0
+	#print "len=", length, len(data)-2
+	#print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked)
+	#print xxd(data)
+	if fin and is_text_frame and has_one_byte_length:
+		if masked:
+			return code_payload(data[6:], data[2:6])
+		else:
+			return data[2:]
+
+#Useful links for ideas on WebSockets:
+#  http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side
+#  https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server
+#  http://tools.ietf.org/html/rfc6455#section-5.2	
+
+
+def flush(myself):
+	lR,lW,lX = select.select([],[myself.connection,],[],60)
+	
+
+def send(myself, data, begin_id="", debug=0):
+	base_frame_size=35000 #could guess by MTU?
+	debug=0
+	#try:
+	while True:
+		counter=0
+		from_end=len(data)-counter
+		if from_end+len(begin_id)>base_frame_size:
+			data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)]
+			header=get_header(len(data_to_send))
+			flush(myself)
+			myself.connection.send(header+data_to_send)
+			if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header))
+		else:
+			data_to_send=begin_id+data[counter:]
+			header=get_header(len(data_to_send))
+			flush(myself)
+			myself.connection.send(header+data_to_send)
+			if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header))
+			#if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send)
+			break
+		counter+=base_frame_size-len(begin_id)
+	#except:
+	#	pass
diff --git a/screenshot b/screenshot
new file mode 100644
index 0000000000000000000000000000000000000000..6344776e1a6932e77056d8adf076c802f2ffc6b3
Binary files /dev/null and b/screenshot differ