WhatsApp online notifier

Notifies you when a user comes online. This script will only work if have a tab open and navigated to https://web.whatsapp.com/ and have selected a person. The next time this person comes online you should hear a bleeb. *Bleeb*!

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          WhatsApp online notifier
// @namespace     https://bennyjacobs.nl/userscripts/WhatsApp-Online-Notifier
// @description	  Notifies you when a user comes online. This script will only work if have a tab open and navigated to https://web.whatsapp.com/ and have selected a person. The next time this person comes online you should hear a bleeb. *Bleeb*!
// @include       https://web.whatsapp.com/
// @version       0.0.2
// @grant         none
// ==/UserScript==
// 
//	The MIT License (MIT)
//	
//	Copyright (c) 2015 Loov
//	
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//	
//	The above copyright notice and this permission notice shall be included in all
//	copies or substantial portions of the Software.
//	
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//	SOFTWARE.
//	
(function(jsfx){
	'use strict';

	var chr = String.fromCharCode;
	var TAU = +Math.PI*2;
	var bitsPerSample = 16|0;
	var numChannels = 1|0;
	var sin = Math.sin;
	var pow = Math.pow;
	var abs = Math.abs;
	var EPSILON = 0.000001;

	jsfx.SampleRate = 0|0;
	jsfx.Sec = 0|0;

	jsfx.SetSampleRate = function(sampleRate){
		jsfx.SampleRate = sampleRate|0;
		jsfx.Sec = sampleRate|0;
	};
	jsfx.SetSampleRate(getDefaultSampleRate());

	// MAIN API

	// Creates a new Audio object based on the params
	// params can be a params generating function or the actual parameters
	jsfx.Sound = function(params){
		var processor = new Processor(params, jsfx.DefaultModules);
		var block = createFloatArray(processor.getSamplesLeft());
		processor.generate(block);
		return CreateAudio(block);
	};

	// Same as Sounds, but avoids locking the browser for too long
	// in case you have a large amount of sounds to generate
	jsfx.Sounds = function(library, ondone, onprogress){
		var audio  = {};
		var player = {};
		player._audio = audio;

		var toLoad = [];

		// create playing functions
		map_object(library, function(_, name){
			player[name] = function(){
				if(typeof audio[name] !== "undefined"){
					audio[name].currentTime = 0.0;
					audio[name].play();
				}
			};
			toLoad.push(name);
		});

		var loaded = 0, total = toLoad.length;
		function next(){
			if(toLoad.length == 0){
				ondone && ondone(sounds);
				return;
			}
			var name = toLoad.shift();
			audio[name] = jsfx.Sound(library[name]);
			loaded++;
			onprogress && onprogress(name, loaded, total);

			window.setTimeout(next, 30);
		}
		next();

		return player;
	}

	// SoundsImmediate takes a named set of params, and generates multiple
	// sound objects at once.
	jsfx.SoundsImmediate = function(library){
		var audio = {};
		var player = {};
		player._audio = audio;
		map_object(library, function(params, name){
			audio[name] = jsfx.Sound(params);
			player[name] = function(){
				if(typeof audio[name] !== "undefined"){
					audio[name].currentTime = 0.0;
					audio[name].play();
				}
			};
		})
		return player;
	};

	if(typeof AudioContext !== "undefined"){
		// Node creates a new AudioContext ScriptProcessor that outputs the
		// sound. It will automatically disconnect, unless otherwise specified.
		jsfx.Node = function(audioContext, params, modules, bufferSize, stayConnected){
			var node = audioContext.createScriptProcessor(bufferSize, 0, 1);
			var gen = new Processor(params, modules || jsfx.DefaultModules);
			node.onaudioprocess = function(ev){
				var block = ev.outputBuffer.getChannelData(0);
				gen.generate(block);
				if(!stayConnected && gen.finished){
					// we need to do an async disconnect, otherwise Chrome may
					// glitch
					setTimeout(function(){ node.disconnect(); }, 30);
				}
			}
			return node;
		}

		// Live creates an managed AudioContext for playing.
		// This is useful, when you want to use procedurally generated sounds.
		jsfx.Live = function(library, modules, BufferSize){
			//TODO: add limit for number of notes played at the same time
			BufferSize = BufferSize || 2048;
			var player = {};

			var context = new AudioContext();
			var volume = context.createGain();
			volume.connect(context.destination);

			player._context = context;
			player._volume = volume;

			map_object(library, function(params, name){
				player[name] =  function(){
					var node = jsfx.Node(context, params, modules, BufferSize);
					node.connect(volume);
				};
			});

			player._close = function(){
				context.close();
			};

			player._play = function(params){
				var node = jsfx.Node(context, params, modules, BufferSize);
				node.connect(volume);
			};

			return player;
		}
	} else {
		jsfx.Live = jsfx.Sounds;
	}

	// SOUND GENERATION
	jsfx.Module = {};

	// generators
	jsfx.G = {};

	var stage = jsfx.stage = {
		PhaseSpeed    : 0,
		PhaseSpeedMod : 10,
		Generator     : 20,
		SampleMod     : 30,
		Volume        : 40
	};
	function byStage(a,b){ return a.stage - b.stage; }

	jsfx.InitDefaultParams = InitDefaultParams;
	function InitDefaultParams(params, modules){
		// setup modules
		for(var i = 0; i < modules.length; i += 1){
			var M = modules[i];
			var P = params[M.name] || {};

			// add missing parameters
			map_object(M.params, function(def, name){
				if(typeof P[name] === 'undefined'){
					P[name] = def.D;
				}
			});

			params[M.name] = P;
		}
	}

	// Generates a stateful sound effect processor
	// params can be a function that creates a parameter set
	jsfx.Processor = Processor;
	function Processor(params, modules){
		params = params || {};
		modules = modules || jsfx.DefaultModules;

		if(typeof params === 'function'){
			params = params();
		} else {
			params = JSON.parse(JSON.stringify(params))
		}
		this.finished = false;

		this.state = {
			SampleRate: params.SampleRate || jsfx.SampleRate
		};

		// sort modules
		modules = modules.slice();
		modules.sort(byStage)
		this.modules = modules;

		// init missing params
		InitDefaultParams(params, modules);

		// setup modules
		for(var i = 0; i < this.modules.length; i += 1){
			var M = this.modules[i];
			this.modules[i].setup(this.state, params[M.name]);
		}
	}
	Processor.prototype = {
		//TODO: see whether this can be converted to a module
		generate: function(block){
			for(var i = 0|0; i < block.length; i += 1){
				block[i] = 0;
			}
			if(this.finished){ return; }

			var $ = this.state,
				N = block.length|0;
			for(var i = 0; i < this.modules.length; i += 1){
				var M = this.modules[i];
				var n = M.process($, block.subarray(0,N))|0;
				N = Math.min(N, n);
			}
			if(N < block.length){
				this.finished = true;
			}
			for(var i = N; i < block.length; i++){
				block[i] = 0;
			}
		},
		getSamplesLeft: function(){
			var samples = 0;
			for(var i = 0; i < this.state.envelopes.length; i += 1){
				samples += this.state.envelopes[i].N;
			}
			if(samples === 0){
				samples = 3*this.state.SampleRate;
			}
			return samples;
		}
	};

	// Frequency
	jsfx.Module.Frequency = {
		name: 'Frequency',
		params: {
			Start: { L:30, H:1800, D:440  },

			Min: { L:30, H:1800, D:30    },
			Max: { L:30, H:1800, D:1800  },

			Slide:      { L:-1, H:1, D:0 },
			DeltaSlide: { L:-1, H:1, D:0 },

			RepeatSpeed:  { L:0, H: 3.0, D: 0 },

			ChangeAmount: { L:-12, H:12, D:0 },
			ChangeSpeed : { L:  0, H:1,  D:0 }
		},
		stage: stage.PhaseSpeed,
		setup: function($, P){
			var SR = $.SampleRate;

			$.phaseParams = P;

			$.phaseSpeed    = P.Start * TAU / SR;
			$.phaseSpeedMax = P.Max * TAU / SR;
			$.phaseSpeedMin = P.Min * TAU / SR;

			$.phaseSpeedMin = Math.min($.phaseSpeedMin, $.phaseSpeed);
			$.phaseSpeedMax = Math.max($.phaseSpeedMax, $.phaseSpeed);

			$.phaseSlide = 1.0 + pow(P.Slide, 3.0) * 64.0 / SR;
			$.phaseDeltaSlide = pow(P.DeltaSlide, 3.0) / (SR * 1000);

			$.repeatTime = 0;
			$.repeatLimit = Infinity;
			if(P.RepeatSpeed > 0){
				$.repeatLimit = P.RepeatSpeed * SR;
			}

			$.arpeggiatorTime = 0;
			$.arpeggiatorLimit = P.ChangeSpeed * SR;
			if(P.ChangeAmount == 0){
				$.arpeggiatorLimit = Infinity;
			}
			$.arpeggiatorMod = 1 + P.ChangeAmount / 12.0;
		},
		process: function($, block){
			var speed = +$.phaseSpeed,
				min   = +$.phaseSpeedMin,
				max   = +$.phaseSpeedMax,
				slide = +$.phaseSlide,
				deltaSlide = +$.phaseDeltaSlide;

			var repeatTime  = $.repeatTime,
				repeatLimit = $.repeatLimit;

			var arpTime  = $.arpeggiatorTime,
				arpLimit = $.arpeggiatorLimit,
				arpMod   = $.arpeggiatorMod;

			for(var i = 0; i < block.length; i++){
				slide += deltaSlide;
				speed *= slide;
				speed = speed < min ? min : speed > max ? max : speed;

				if(repeatTime > repeatLimit){
					this.setup($, $.phaseParams);
					return i + this.process($, block.subarray(i)) - 1;
				}
				repeatTime++;

				if(arpTime > arpLimit){
					speed *= arpMod;
					arpTime = 0;
					arpLimit = Infinity;
				}
				arpTime++;

				block[i] += speed;
			}

			$.repeatTime = repeatTime;
			$.arpeggiatorTime = arpTime;
			$.arpeggiatorLimit = arpLimit;

			$.phaseSpeed = speed;
			$.phaseSlide = slide;

			return block.length;
		}
	};

	// Vibrato
	jsfx.Module.Vibrato = {
		name: 'Vibrato',
		params: {
			Depth:      {L: 0, H:1, D:0},
			DepthSlide: {L:-1, H:1, D:0},

			Frequency:      {L:  0.01, H:48, D:0},
			FrequencySlide: {L: -1.00, H: 1, D:0}
		},
		stage: stage.PhaseSpeedMod,
		setup: function($, P){
			var SR = $.SampleRate;
			$.vibratoPhase = 0;
			$.vibratoDepth = P.Depth;
			$.vibratoPhaseSpeed = P.Frequency * TAU / SR;

			$.vibratoPhaseSpeedSlide = 1.0 + pow(P.FrequencySlide, 3.0) * 3.0 / SR;
			$.vibratoDepthSlide = P.DepthSlide / SR;
		},
		process: function($, block){
			var phase = +$.vibratoPhase,
				depth = +$.vibratoDepth,
				speed = +$.vibratoPhaseSpeed,
				slide = +$.vibratoPhaseSpeedSlide,
				depthSlide = +$.vibratoDepthSlide;

			if((depth == 0) && (depthSlide <= 0)){
				return block.length;
			}

			for(var i = 0; i < block.length; i++){
				phase += speed;
				if(phase > TAU){phase -= TAU};
				block[i] += block[i] * sin(phase) * depth;

				speed *= slide;
				depth += depthSlide;
				depth = clamp1(depth);
			}

			$.vibratoPhase = phase;
			$.vibratoDepth = depth;
			$.vibratoPhaseSpeed = speed;
			return block.length;
		}
	};

	// Generator
	jsfx.Module.Generator = {
		name: 'Generator',
		params: {
			// C = choose
			Func: {C: jsfx.G, D:'square'},

			A: {L: 0, H: 1, D: 0},
			B: {L: 0, H: 1, D: 0},

			ASlide: {L: -1, H: 1, D: 0},
			BSlide: {L: -1, H: 1, D: 0}
		},
		stage: stage.Generator,
		setup: function($, P){
			$.generatorPhase = 0;

			if(typeof P.Func === 'string'){
				$.generator = jsfx.G[P.Func];
			} else {
				$.generator = P.Func;
			}
			if(typeof $.generator === 'object'){
				$.generator = $.generator.create();
			}
			assert(typeof $.generator === 'function', 'generator must be a function')

			$.generatorA = P.A;
			$.generatorASlide = P.ASlide;
			$.generatorB = P.B;
			$.generatorBSlide = P.BSlide;
		},
		process: function($, block){
			return $.generator($, block);
		}
	};

	// Karplus Strong algorithm for string sound
	var GuitarBufferSize = 1 << 16;
	jsfx.Module.Guitar = {
		name: 'Guitar',
		params: {
			A: {L:0.0, H:1.0, D: 1},
			B: {L:0.0, H:1.0, D: 1},
			C: {L:0.0, H:1.0, D: 1},
		},
		stage: stage.Generator,
		setup: function($, P){
			$.guitarA = P.A;
			$.guitarB = P.B;
			$.guitarC = P.C;

			$.guitarBuffer = createFloatArray(GuitarBufferSize);
			$.guitarHead = 0;
			var B = $.guitarBuffer;
			for(var i = 0; i < B.length; i++){
				B[i] = Math.random()*2 - 1;
			}
		},
		process: function($, block){
			var BS = GuitarBufferSize,
				BM = BS - 1;

			var A = +$.guitarA, B = +$.guitarB, C = +$.guitarC;
			var T = A + B + C;
			var h = $.guitarHead;

			var buffer = $.guitarBuffer;
			for(var i = 0; i < block.length; i++){
				// buffer size
				var n = (TAU / block[i])|0;
				n = n > BS ? BS : n;

				// tail
				var t = ((h - n) + BS) & BM;
				buffer[h] =
					(buffer[(t-0+BS)&BM]*A +
					 buffer[(t-1+BS)&BM]*B +
					 buffer[(t-2+BS)&BM]*C) / T;

				block[i] = buffer[h];
				h = (h + 1) & BM;
			}

			$.guitarHead = h;
			return block.length;
		}
	}

	// Low/High-Pass Filter
	jsfx.Module.Filter = {
		name: 'Filter',
		params: {
			LP:          {L: 0, H:1, D:1},
			LPSlide:     {L:-1, H:1, D:0},
			LPResonance: {L: 0, H:1, D:0},
			HP:          {L: 0, H:1, D:0},
			HPSlide:     {L:-1, H:1, D:0}
		},
		stage: stage.SampleMod + 0,
		setup: function($, P){
			$.FilterEnabled = (P.HP > EPSILON) || (P.LP < 1 - EPSILON);

			$.LPEnabled = P.LP < 1 - EPSILON;
			$.LP = pow(P.LP, 3.0) / 10;
			$.LPSlide = 1.0 + P.LPSlide * 100 / $.SampleRate;
			$.LPPos = 0;
			$.LPPosSlide = 0;

			$.LPDamping = 5.0 / (1.0 + pow(P.LPResonance, 2) * 20) * (0.01 + P.LP);
			$.LPDamping = 1.0 - Math.min($.LPDamping, 0.8);

			$.HP = pow(P.HP, 2.0) / 10;
			$.HPPos = 0;
			$.HPSlide = 1.0 + P.HPSlide * 100 / $.SampleRate;
		},
		enabled: function($){
			return $.FilterEnabled;
		},
		process: function($, block){
			if(!this.enabled($)){ return block.length; }

			var lp         = +$.LP;
			var lpPos      = +$.LPPos;
			var lpPosSlide = +$.LPPosSlide;
			var lpSlide    = +$.LPSlide;
			var lpDamping  = +$.LPDamping;
			var lpEnabled  = +$.LPEnabled;

			var hp      = +$.HP;
			var hpPos   = +$.HPPos;
			var hpSlide = +$.HPSlide;

			for(var i = 0; i < block.length; i++){
				if((hp > EPSILON) || (hp < -EPSILON)){
					hp *= hpSlide;
					hp = hp < EPSILON ? EPSILON: hp > 0.1 ? 0.1 : hp;
				}

				var lpPos_ = lpPos;

				lp *= lpSlide;
				lp = lp < 0 ? lp = 0 : lp > 0.1 ? 0.1 : lp;

				var sample = block[i];
				if(lpEnabled){
					lpPosSlide += (sample - lpPos) * lp;
					lpPosSlide *= lpDamping;
				} else {
					lpPos = sample;
					lpPosSlide = 0;
				}
				lpPos += lpPosSlide;

				hpPos += lpPos - lpPos_;
				hpPos *= 1.0 - hp;

				block[i] = hpPos;
			}

			$.LPPos = lpPos;
			$.LPPosSlide = lpPosSlide;
			$.LP = lp;
			$.HP = hp;
			$.HPPos = hpPos;

			return block.length;
		}
	};

	// Phaser Effect
	var PhaserBufferSize = 1 << 10;
	jsfx.Module.Phaser = {
		name: 'Phaser',
		params: {
			Offset: {L:-1, H:1, D:0},
			Sweep:  {L:-1, H:1, D:0}
		},
		stage: stage.SampleMod + 1,
		setup: function($, P){
			$.phaserBuffer = createFloatArray(PhaserBufferSize);
			$.phaserPos  = 0;
			$.phaserOffset = pow(P.Offset, 2.0) * (PhaserBufferSize - 4);
			$.phaserOffsetSlide = pow(P.Sweep, 3.0) * 4000 / $.SampleRate;
		},
		enabled: function($){
			return (abs($.phaserOffsetSlide) > EPSILON) ||
				(abs($.phaserOffset) > EPSILON);
		},
		process: function($, block){
			if(!this.enabled($)){ return block.length; }

			var BS = PhaserBufferSize,
				BM = BS - 1;

			var buffer = $.phaserBuffer,
				pos    = $.phaserPos|0,
				offset = +$.phaserOffset,
				offsetSlide = +$.phaserOffsetSlide;

			for(var i = 0; i < block.length; i++){
				offset += offsetSlide;
				//TODO: check whether this is correct
				if(offset < 0){
					offset = -offset;
					offsetSlide = -offsetSlide;
				}
				if(offset > BM){
					offset = BM;
					offsetSlide = 0;
				}

				buffer[pos] = block[i];
				var p = (pos - (offset|0) + BS) & BM;
				block[i] += buffer[p];

				pos = ((pos + 1) & BM)|0;
			}

			$.phaserPos = pos;
			$.phaserOffset = offset;
			return block.length;
		}
	};

	// Volume dynamic control with Attack-Sustain-Decay
	//   ATTACK  | 0              - Volume + Punch
	//   SUSTAIN | Volume + Punch - Volume
	//   DECAY   | Volume         - 0
	jsfx.Module.Volume = {
		name: 'Volume',
		params: {
			Master:  { L: 0, H: 1, D: 0.5 },
			Attack:  { L: 0.001, H: 1, D: 0.01 },
			Sustain: { L: 0, H: 2, D: 0.3 },
			Punch:   { L: 0, H: 3, D: 1.0 },
			Decay:   { L: 0.001, H: 2, D: 1.0 }
		},
		stage: stage.Volume,
		setup: function($, P){
			var SR = $.SampleRate;
			var V = P.Master;
			var VP = V * (1 + P.Punch);
			$.envelopes = [
				// S = start volume, E = end volume, N = duration in samples
				{S: 0, E: V, N: (P.Attack  * SR)|0 }, // Attack
				{S:VP, E: V, N: (P.Sustain * SR)|0 }, // Sustain
				{S: V, E: 0, N: (P.Decay   * SR)|0 }  // Decay
			];
			// G = volume gradient
			for(var i = 0; i < $.envelopes.length; i += 1){
				var e = $.envelopes[i];
				e.G = (e.E - e.S) / e.N;
			}
		},
		process: function($, block){
			var i = 0;
			while(($.envelopes.length > 0) && (i < block.length)){
				var E = $.envelopes[0];
				var vol = E.S,
					grad = E.G;

				var N = Math.min(block.length - i, E.N)|0;
				var end = (i+N)|0;
				for(; i < end; i += 1){
					block[i] *= vol;
					vol += grad;
					vol = clamp(vol, 0, 10);
				}
				E.S = vol;
				E.N -= N;
				if(E.N <= 0){
					$.envelopes.shift();
				}
			}
			return i;
		}
	};

	// PRESETS

	jsfx.DefaultModules = [
		jsfx.Module.Frequency,
		jsfx.Module.Vibrato,
		jsfx.Module.Generator,
		jsfx.Module.Filter,
		jsfx.Module.Phaser,
		jsfx.Module.Volume
	];
	jsfx.DefaultModules.sort(byStage);

	jsfx.EmptyParams = EmptyParams;
	function EmptyParams(){
		return map_object(jsfx.Module, function(){ return {} });
	}

	jsfx._RemoveEmptyParams = RemoveEmptyParams;
	function RemoveEmptyParams(params){
		for(var name in params){
			if(Object_keys(params[name]).length == 0){
				delete params[name];
			}
		}
	};

	jsfx.Preset = {
		Reset: function(){
			return EmptyParams();
		},
		Coin: function(){
			var p = EmptyParams();
			p.Frequency.Start = runif(880, 660);
			p.Volume.Sustain = runif(0.1);
			p.Volume.Decay = runif(0.4, 0.1);
			p.Volume.Punch = runif(0.3, 0.3);
			if(runif() < 0.5){
				p.Frequency.ChangeSpeed = runif(0.15, 0.1);
				p.Frequency.ChangeAmount = runif(8, 4);
			}
			RemoveEmptyParams(p);
			return p;
		},
		Laser: function(){
			var p = EmptyParams();
			p.Generator.Func = rchoose(['square', 'saw', 'sine']);

			if(runif() < 0.33){
				p.Frequency.Start = runif(880, 440);
				p.Frequency.Min = runif(0.1);
				p.Frequency.Slide = runif(0.3, -0.8);
			} else {
				p.Frequency.Start = runif(1200, 440);
				p.Frequency.Min = p.Frequency.Start - runif(880, 440);
				if(p.Frequency.Min < 110){ p.Frequency.Min = 110; }
				p.Frequency.Slide = runif(0.3, -1);
			}

			if(runif() < 0.5){
				p.Generator.A = runif(0.5);
				p.Generator.ASlide = runif(0.2);
			} else {
				p.Generator.A = runif(0.5, 0.4);
				p.Generator.ASlide = runif(0.7);
			}

			p.Volume.Sustain = runif(0.2, 0.1);
			p.Volume.Decay   = runif(0.4);
			if(runif() < 0.5){
				p.Volume.Punch = runif(0.3);
			}
			if(runif() < 0.33){
				p.Phaser.Offset = runif(0.2);
				p.Phaser.Sweep = runif(0.2);
			}
			if(runif() < 0.5){
				p.Filter.HP = runif(0.3);
			}
			RemoveEmptyParams(p);
			return p;
		},
		Explosion: function(){
			var p = EmptyParams();
			p.Generator.Func = 'noise';
			if(runif() < 0.5){
				p.Frequency.Start = runif(440, 40);
				p.Frequency.Slide = runif(0.4, -0.1);
			} else {
				p.Frequency.Start = runif(1600, 220);
				p.Frequency.Slide = runif(-0.2, -0.2);
			}

			if(runif() < 0.2){ p.Frequency.Slide = 0; }
			if(runif() < 0.3){ p.Frequency.RepeatSpeed = runif(0.5, 0.3); }

			p.Volume.Sustain = runif(0.3, 0.1);
			p.Volume.Decay   = runif(0.5);
			p.Volume.Punch   = runif(0.6, 0.2);

			if(runif() < 0.5){
				p.Phaser.Offset = runif(0.9, -0.3);
				p.Phaser.Sweep  = runif(-0.3);
			}

			if(runif() < 0.33){
				p.Frequency.ChangeSpeed = runif(0.3, 0.6);
				p.Frequency.ChangeAmount = runif(24, -12);
			}
			RemoveEmptyParams(p);
			return p;
		},
		Powerup: function(){
			var p = EmptyParams();
			if(runif() < 0.5){
				p.Generator.Func = 'saw';
			} else {
				p.Generator.A = runif(0.6);
			}

			p.Frequency.Start = runif(220, 440);
			if(runif() < 0.5){
				p.Frequency.Slide = runif(0.5, 0.2);
				p.Frequency.RepeatSpeed = runif(0.4, 0.4);
			} else {
				p.Frequency.Slide = runif(0.2, 0.05);
				if(runif() < 0.5){
					p.Vibrato.Depth = runif(0.6, 0.1);
					p.Vibrato.Frequency = runif(30, 10);
				}
			}

			p.Volume.Sustain = runif(0.4);
			p.Volume.Decay = runif(0.4, 0.1);

			RemoveEmptyParams(p);
			return p;
		},
		Hit: function(){
			var p = EmptyParams();
			p.Generator.Func = rchoose(['square', 'saw', 'noise']);
			p.Generator.A = runif(0.6);
			p.Generator.ASlide = runif(1, -0.5);

			p.Frequency.Start = runif(880, 220);
			p.Frequency.Slide = -runif(0.4, 0.3);

			p.Volume.Sustain = runif(0.1);
			p.Volume.Decay = runif(0.2, 0.1);

			if(runif() < 0.5){
				p.Filter.HP = runif(0.3);
			}

			RemoveEmptyParams(p);
			return p;
		},
		Jump: function(){
			var p = EmptyParams();
			p.Generator.Func = 'square';
			p.Generator.A = runif(0.6);

			p.Frequency.Start = runif(330, 330);
			p.Frequency.Slide = runif(0.4, 0.2);

			p.Volume.Sustain = runif(0.3, 0.1);
			p.Volume.Decay = runif(0.2, 0.1);

			if(runif() < 0.5){
				p.Filter.HP = runif(0.3);
			}
			if(runif() < 0.3){
				p.Filter.LP = runif(-0.6, 1);
			}

			RemoveEmptyParams(p);
			return p;
		},
		Select: function(){
			var p = EmptyParams();
			p.Generator.Func = rchoose(['square', 'saw']);
			p.Generator.A = runif(0.6);

			p.Frequency.Start = runif(660, 220);

			p.Volume.Sustain = runif(0.1, 0.1);
			p.Volume.Decay   = runif(0.2);

			p.Filter.HP = 0.2;
			RemoveEmptyParams(p);
			return p;
		},
		Lucky: function(){
			var p = EmptyParams();
			map_object(p, function(out, moduleName){
				var defs = jsfx.Module[moduleName].params;
				map_object(defs, function(def, name){
					if(def.C){
						var values = Object_keys(def.C);
						out[name] = values[(values.length * Math.random()) | 0];
					} else {
						out[name] = Math.random() * (def.H - def.L) + def.L;
					}
				});
			});
			p.Volume.Master = 0.4;
			p.Filter = {}; // disable filter, as it usually will clip everything
			RemoveEmptyParams(p);
			return p;
		}
	};

	// GENERATORS

	// uniform noise
	jsfx.G.unoise = newGenerator("sample = Math.random();");
	// sine wave
	jsfx.G.sine = newGenerator("sample = Math.sin(phase);");
	// saw wave
	jsfx.G.saw = newGenerator("sample = 2*(phase/TAU - ((phase/TAU + 0.5)|0));");
	// triangle wave
	jsfx.G.triangle = newGenerator("sample = Math.abs(4 * ((phase/TAU - 0.25)%1) - 2) - 1;");
	// square wave
	jsfx.G.square = newGenerator("var s = Math.sin(phase); sample = s > A ? 1.0 : s < A ? -1.0 : A;");
	// simple synth
	jsfx.G.synth = newGenerator("sample = Math.sin(phase) + .5*Math.sin(phase/2) + .3*Math.sin(phase/4);");

	// STATEFUL
	var __noiseLast = 0;
	jsfx.G.noise = newGenerator("if(phase % TAU < 4){__noiseLast = Math.random() * 2 - 1;} sample = __noiseLast;");

	// Karplus-Strong string
	jsfx.G.string = {
		create: function(){
			var BS = 1 << 16;
			var BM = BS-1;

			var buffer = createFloatArray(BS);
			for(var i = 0; i < buffer.length; i++){
				buffer[i] = Math.random()*2-1;
			}

			var head = 0;
			return function($, block){
				var TAU = Math.PI * 2;
				var A = +$.generatorA, ASlide = +$.generatorASlide,
					B = +$.generatorB, BSlide = +$.generatorBSlide;
				var buf = buffer;

				for(var i = 0; i < block.length; i++){
					var phaseSpeed = block[i];
					var n = (TAU/phaseSpeed)|0;
					A += ASlide; B += BSlide;
					A = A < 0 ? 0 : A > 1 ? 1 : A;
					B = B < 0 ? 0 : B > 1 ? 1 : B;

					var t = ((head - n) + BS) & BM;
					var sample = (
						buf[(t-0+BS)&BM]*1 +
						buf[(t-1+BS)&BM]*A +
						buf[(t-2+BS)&BM]*B) / (1+A+B);

					buf[head] = sample;
					block[i] = buf[head];
					head = (head + 1) & BM;
				}

				$.generatorA = A;
				$.generatorB = B;
				return block.length;
			}
		}
	};

	// Generates samples using given frequency and generator
	function newGenerator(line){
		return new Function("$", "block", "" +
			"var TAU = Math.PI * 2;\n" +
			"var sample;\n" +
			"var phase = +$.generatorPhase,\n"+
			"	A = +$.generatorA, ASlide = +$.generatorASlide,\n"+
			"	B = +$.generatorB, BSlide = +$.generatorBSlide;\n"+
			"\n"+
			"for(var i = 0; i < block.length; i++){\n"+
			"	var phaseSpeed = block[i];\n"+
			"	phase += phaseSpeed;\n"+
			"	if(phase > TAU){ phase -= TAU };\n"+
			"	A += ASlide; B += BSlide;\n"+
			"   A = A < 0 ? 0 : A > 1 ? 1 : A;\n"+
			"   B = B < 0 ? 0 : B > 1 ? 1 : B;\n"+
			line +
			"	block[i] = sample;\n"+
			"}\n"+
			"\n"+
			"$.generatorPhase = phase;\n"+
			"$.generatorA = A;\n"+
			"$.generatorB = B;\n"+
			"return block.length;\n" +
		"");
	}

	// WAVE SUPPORT

	// Creates an Audio element from audio data [-1.0 .. 1.0]
	jsfx.CreateAudio = CreateAudio;
	function CreateAudio(data){
		if(typeof Float32Array !== "undefined"){
			assert(data instanceof Float32Array, 'data must be an Float32Array');
		}

		var blockAlign = numChannels * bitsPerSample >> 3;
		var byteRate = jsfx.SampleRate * blockAlign;

		var output = createByteArray(8 + 36 + data.length * 2);
		var p = 0;

		// emits string to output
		function S(value){
			for(var i = 0; i < value.length; i += 1){
				output[p] = value.charCodeAt(i); p++;
			}
		}

		// emits integer value to output
		function V(value, nBytes){
			if(nBytes <= 0){ return; }
			output[p] = value & 0xFF; p++;
			V(value >> 8, nBytes - 1);
		}

		S('RIFF'); V(36 + data.length * 2, 4);

		S('WAVEfmt '); V(16, 4); V(1, 2);
		V(numChannels, 2); V(jsfx.SampleRate, 4);
		V(byteRate, 4); V(blockAlign, 2); V(bitsPerSample, 2);

		S('data'); V(data.length * 2, 4);
		CopyFToU8(output.subarray(p), data);

		return new Audio('data:audio/wav;base64,' + U8ToB64(output));
	};

	jsfx.DownloadAsFile = function(audio){
		assert(audio instanceof Audio, 'input must be an Audio object');
		document.location.href = audio.src;
	};

	// HELPERS
	jsfx.Util = {};

	// Copies array of Floats to a Uint8Array with 16bits per sample
	jsfx.Util.CopyFToU8 = CopyFToU8;
	function CopyFToU8(into, floats){
		assert(into.length/2 == floats.length,
			'the target buffer must be twice as large as the iinput');

		var k = 0;
		for(var i = 0; i < floats.length; i++){
			var v = +floats[i];
			var	a = (v * 0x7FFF)|0;
			a = a < -0x8000 ? -0x8000 : 0x7FFF < a ? 0x7FFF : a;
			a += a < 0 ? 0x10000 : 0;
			into[k] = a & 0xFF; k++;
			into[k] = a >> 8; k++;
		}
	}

	function U8ToB64(data){
		var CHUNK = 0x8000;
		var result = '';
		for(var start = 0; start < data.length; start += CHUNK){
			var end = Math.min(start + CHUNK, data.length);
			result += String.fromCharCode.apply(null, data.subarray(start, end));
		}
		return btoa(result);
	}

	// uses AudioContext sampleRate or 44100;
	function getDefaultSampleRate(){
		if(typeof AudioContext !== 'undefined'){
			return (new AudioContext()).sampleRate;
		}
		return 44100;
	}

	// for checking pre/post conditions
	function assert(condition, message){
		if(!condition){ throw new Error(message); }
	}

	function clamp(v, min, max){
		v = +v; min = +min; max = +max;
		if(v < min){ return +min; }
		if(v > max){ return +max; }
		return +v;
	}

	function clamp1(v){
		v = +v;
		if(v < +0.0){ return +0.0; }
		if(v > +1.0){ return +1.0; }
		return +v;
	}

	function map_object(obj, fn){
		var r = {};
		for(var name in obj){
			if(obj.hasOwnProperty(name)){
				r[name] = fn(obj[name], name);
			}
		}
		return r;
	}

	// uniform random
	function runif(scale, offset){
		var a = Math.random();
        if(scale !== undefined)
            a *= scale;
        if(offset !== undefined)
            a += offset;
        return a;
	}

	function rchoose(gens){
		return gens[(gens.length*Math.random())|0];
	}

	function Object_keys(obj){
		var r = [];
		for(var name in obj){ r.push(name); }
		return r;
	}

	jsfx._createFloatArray = createFloatArray;
	function createFloatArray(N){
		if(typeof Float32Array === "undefined") {
			var r = new Array(N);
			for(var i = 0; i < r.length; i++){
				r[i] = 0.0;
			}
		}
		return new Float32Array(N);
	}

	function createByteArray(N){
		if(typeof Uint8Array === "undefined") {
			var r = new Array(N);
			for(var i = 0; i < r.length; i++){
				r[i] = 0|0;
			}
		}
		return new Uint8Array(N);
	}
})(this.jsfx = {});

var waitForElementId = function(id, cb) {
  
  // Travel the node(s) in a recursive fashion.
  var walk = function(node) {
    var child, next;

    if(node.id ==  id) {
       cb(node);
    }

    switch (node.nodeType) {
      case 1:  // Element
      case 9:  // Document
      case 11: // Document fragment
        child = node.firstChild;
        while (child) {
          next = child.nextSibling;
          walk(child);
          child = next;
        }
        break;
      case 3: // Text node
        break;
      default:
        break;
    }
  };

  var MutationObserver = (window.MutationObserver || window.WebKitMutationObserver);
  var observer = new MutationObserver(function (mutations) {
    mutations.forEach(function (mutation) {
      if(mutation.type == 'childList') {
        for (var i = 0; i < mutation.addedNodes.length; ++i) {
          walk(mutation.addedNodes[i]);
        }
      } else if (mutation.type == 'characterData') {
          walk(mutation.target);
      }
    });
  });

  observer.observe(document, {
    childList: true,
    characterData: true,
    subtree: true,
  });

  walk(document.body);
}

function notifyUser() {
  
  var library = {
      "static": {"Volume":{"Sustain":0.1,"Decay":0.15,"Punch":0.55}},
      "dynamic": function(){
          return {"Frequency": { "Start": Math.random()*440 + 220 }};
      },
      "coin": jsfx.Preset.Coin
  };
  var sfx = jsfx.Live(library);
  
  // Let's check if the browser supports notifications
  if (!("Notification" in window)) {
    alert("This browser does not support desktop notification");
  }

  // Let's check whether notification permissions have already been granted
  else if (Notification.permission === "granted") {
    // If it's okay let's create a notification
    var notification = new Notification("User is online");
    sfx.coin();
  }

  // Otherwise, we need to ask the user for permission
  else if (Notification.permission !== 'denied') {
    Notification.requestPermission(function (permission) {
      // If the user accepts, let's create a notification
      if (permission === "granted") {
        var notification = new Notification("User is online");
        sfx.coin();
      }
    });
  }

  // At last, if the user has denied notifications, and you 
  // want to be respectful there is no need to bother them any more.
}

waitForElementId("main", function(node) {
  
  console.log("Main panel loaded.")
  
  var handleTextNode = function(textNode) {
      if(textNode.textContent == 'online')
      {
        console.log("User is online");
        notifyUser();
      }
  };
  
  var walk = function(node) {
    var child, next;

    switch (node.nodeType) {
      case 1:  // Element
      case 9:  // Document
      case 11: // Document fragment
        child = node.firstChild;
        while (child) {
          next = child.nextSibling;
          walk(child);
          child = next;
        }
        break;
      case 3: // Text node
        handleTextNode(node);
        break;
      default:
        break;
    }
  };

  var MutationObserver = (window.MutationObserver || window.WebKitMutationObserver);
  var observer = new MutationObserver(function (mutations) {
    mutations.forEach(function (mutation) {
      if(mutation.type == 'childList') {
        for (var i = 0; i < mutation.addedNodes.length; ++i) {
          walk(mutation.addedNodes[i]);
        }
      } else if (mutation.type == 'characterData') {
          handleTextNode(mutation.target);
      }
    });
  });

  observer.observe(node, {
    childList: true,
    characterData: true,
    subtree: true,
  });

  walk(node);
})