Browse Source

第一次上传

LiuLiu 3 years ago
parent
commit
886a610519
100 changed files with 5925 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 1 0
      MIDI.js
  3. BIN
      images/huaji1.png
  4. BIN
      images/huaji2.png
  5. 24 0
      inc/jasmid/LICENSE
  6. 238 0
      inc/jasmid/midifile.js
  7. 102 0
      inc/jasmid/replayer.js
  8. 69 0
      inc/jasmid/stream.js
  9. 61 0
      inc/shim/Base64.js
  10. 81 0
      inc/shim/Base64binary.js
  11. 111 0
      inc/shim/WebAudioAPI.js
  12. 421 0
      inc/shim/WebMIDIAPI.js
  13. 179 0
      inc/tuna/README.md
  14. BIN
      inc/tuna/impulses/Sweetspot1M.wav
  15. BIN
      inc/tuna/impulses/impulse_guitar.wav
  16. BIN
      inc/tuna/impulses/impulse_rev.wav
  17. BIN
      inc/tuna/impulses/ir_rev_short.wav
  18. 2093 0
      inc/tuna/tuna.js
  19. 260 0
      index.html
  20. 85 0
      js/midi/audioDetect.js
  21. 161 0
      js/midi/gm.js
  22. 199 0
      js/midi/loader.js
  23. 192 0
      js/midi/player.js
  24. 387 0
      js/midi/player2.js
  25. 150 0
      js/midi/plugin.audiotag.js
  26. 326 0
      js/midi/plugin.webaudio.js
  27. 93 0
      js/midi/plugin.webmidi.js
  28. 320 0
      js/midi/synesthesia.js
  29. 225 0
      js/util/dom_request_script.js
  30. 146 0
      js/util/dom_request_xhr.js
  31. 0 0
      midi.min.js
  32. BIN
      midis/AQUA.mid
  33. BIN
      midis/Again.mid
  34. BIN
      midis/All in good time.mid
  35. BIN
      midis/Allegro Cantabile Sound.mid
  36. BIN
      midis/Amnanesis.mid
  37. BIN
      midis/Arifutarekanashiminohate.mid
  38. BIN
      midis/Aruji Naki Sono Koe.mid
  39. BIN
      midis/Ashita e no Kaerimichi.mid
  40. BIN
      midis/Astronomia.mid
  41. BIN
      midis/Attack on Titan.mid
  42. BIN
      midis/Before my body is dry.mid
  43. BIN
      midis/Bios.mid
  44. BIN
      midis/Blessing.mid
  45. BIN
      midis/Blue Bird.mid
  46. BIN
      midis/Blumenkranz.mid
  47. BIN
      midis/Boku ja nai.mid
  48. BIN
      midis/Boys be Smile.mid
  49. BIN
      midis/Brave Heart.mid
  50. BIN
      midis/Brave Shine.mid
  51. BIN
      midis/Brave Song (piano + viola).mid
  52. BIN
      midis/Brave Song.mid
  53. BIN
      midis/Butterfly.mid
  54. BIN
      midis/COLORS.mid
  55. BIN
      midis/Cha-la Head-Cha-La.mid
  56. BIN
      midis/Challenge accepted (1).mid
  57. BIN
      midis/City of Eternity.mid
  58. BIN
      midis/Cras numquam scire.mid
  59. BIN
      midis/Daydream Syndrome.mid
  60. BIN
      midis/Dream theme from Nazo no Kanojo X.mid
  61. BIN
      midis/EONIAN.mid
  62. BIN
      midis/Enter Enter Mission.mid
  63. BIN
      midis/Esoragoto.mid
  64. BIN
      midis/Euterpe.mid
  65. BIN
      midis/Everyday World.mid
  66. BIN
      midis/Extra magic hour.mid
  67. BIN
      midis/Fallen Angel.mid
  68. BIN
      midis/Fubuki.mid
  69. BIN
      midis/GO GO Maniac.mid
  70. BIN
      midis/Gotta catch 'em all.mid
  71. BIN
      midis/Guren no Yumiya.mid
  72. BIN
      midis/Heartwarming.mid
  73. BIN
      midis/Hello Alone -Yui Ballade-.mid
  74. BIN
      midis/Hikari no Senritsu.mid
  75. BIN
      midis/Hikaru Nara.mid
  76. BIN
      midis/Jiyuu no Tsubasa for two pianos (MIDI).mid
  77. BIN
      midis/Kanashimi no ato ni.mid
  78. BIN
      midis/Kancolle - Piano Suite.mid
  79. BIN
      midis/Key anime piano suite.mid
  80. BIN
      midis/Kibou ni Tsuite.mid
  81. BIN
      midis/Kibou no Hana.mid
  82. BIN
      midis/Killy Killy Joker.mid
  83. BIN
      midis/Kimi ni Matsuwaru Mystery.mid
  84. BIN
      midis/Kimi no Shiranai Monogatari.mid
  85. BIN
      midis/Koibumi.mid
  86. BIN
      midis/Koko Kara Hajimaru Monogatari.mid
  87. BIN
      midis/Kokoro no Senritsu.mid
  88. BIN
      midis/Kono Namida wo Kimi ni Sasagu (2).mid
  89. BIN
      midis/Kuchizuke Diamond.mid
  90. BIN
      midis/Kuusou Mesorogiwi.mid
  91. BIN
      midis/Level 5 - Judgelight.mid
  92. BIN
      midis/Life goes on.mid
  93. BIN
      midis/Light my Fire.mid
  94. BIN
      midis/Little Busters!.mid
  95. BIN
      midis/Lumis Eterne.mid
  96. BIN
      midis/Madoka Magica - Piano Medley.mid
  97. BIN
      midis/Magia.mid
  98. BIN
      midis/Main Theme from Non Non Byori.mid
  99. BIN
      midis/Masshiro World.mid
  100. BIN
      midis/Megumeru - Cuckool mix 2007 -.mid

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.history

+ 1 - 0
MIDI.js

@@ -0,0 +1 @@
+Subproject commit a8a84257afa70721ae462448048a87301fc1554a

BIN
images/huaji1.png


BIN
images/huaji2.png


+ 24 - 0
inc/jasmid/LICENSE

@@ -0,0 +1,24 @@
+Copyright (c) 2010, Matt Westcott & Ben Firshman
+All rights reserved.
+ 
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+ 
+ * Redistributions of source code must retain the above copyright notice, this 
+   list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice, 
+   this list of conditions and the following disclaimer in the documentation 
+   and/or other materials provided with the distribution.
+ * The names of its contributors may not be used to endorse or promote products 
+   derived from this software without specific prior written permission.
+ 
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 238 - 0
inc/jasmid/midifile.js

@@ -0,0 +1,238 @@
+/*
+class to parse the .mid file format
+(depends on stream.js)
+*/
+function MidiFile(data) {
+	function readChunk(stream) {
+		var id = stream.read(4);
+		var length = stream.readInt32();
+		return {
+			'id': id,
+			'length': length,
+			'data': stream.read(length)
+		};
+	}
+	
+	var lastEventTypeByte;
+	
+	function readEvent(stream) {
+		var event = {};
+		event.deltaTime = stream.readVarInt();
+		var eventTypeByte = stream.readInt8();
+		if ((eventTypeByte & 0xf0) == 0xf0) {
+			/* system / meta event */
+			if (eventTypeByte == 0xff) {
+				/* meta event */
+				event.type = 'meta';
+				var subtypeByte = stream.readInt8();
+				var length = stream.readVarInt();
+				switch(subtypeByte) {
+					case 0x00:
+						event.subtype = 'sequenceNumber';
+						if (length != 2) throw "Expected length for sequenceNumber event is 2, got " + length;
+						event.number = stream.readInt16();
+						return event;
+					case 0x01:
+						event.subtype = 'text';
+						event.text = stream.read(length);
+						return event;
+					case 0x02:
+						event.subtype = 'copyrightNotice';
+						event.text = stream.read(length);
+						return event;
+					case 0x03:
+						event.subtype = 'trackName';
+						event.text = stream.read(length);
+						return event;
+					case 0x04:
+						event.subtype = 'instrumentName';
+						event.text = stream.read(length);
+						return event;
+					case 0x05:
+						event.subtype = 'lyrics';
+						event.text = stream.read(length);
+						return event;
+					case 0x06:
+						event.subtype = 'marker';
+						event.text = stream.read(length);
+						return event;
+					case 0x07:
+						event.subtype = 'cuePoint';
+						event.text = stream.read(length);
+						return event;
+					case 0x20:
+						event.subtype = 'midiChannelPrefix';
+						if (length != 1) throw "Expected length for midiChannelPrefix event is 1, got " + length;
+						event.channel = stream.readInt8();
+						return event;
+					case 0x2f:
+						event.subtype = 'endOfTrack';
+						if (length != 0) throw "Expected length for endOfTrack event is 0, got " + length;
+						return event;
+					case 0x51:
+						event.subtype = 'setTempo';
+						if (length != 3) throw "Expected length for setTempo event is 3, got " + length;
+						event.microsecondsPerBeat = (
+							(stream.readInt8() << 16)
+							+ (stream.readInt8() << 8)
+							+ stream.readInt8()
+						)
+						return event;
+					case 0x54:
+						event.subtype = 'smpteOffset';
+						if (length != 5) throw "Expected length for smpteOffset event is 5, got " + length;
+						var hourByte = stream.readInt8();
+						event.frameRate = {
+							0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30
+						}[hourByte & 0x60];
+						event.hour = hourByte & 0x1f;
+						event.min = stream.readInt8();
+						event.sec = stream.readInt8();
+						event.frame = stream.readInt8();
+						event.subframe = stream.readInt8();
+						return event;
+					case 0x58:
+						event.subtype = 'timeSignature';
+						if (length != 4) throw "Expected length for timeSignature event is 4, got " + length;
+						event.numerator = stream.readInt8();
+						event.denominator = Math.pow(2, stream.readInt8());
+						event.metronome = stream.readInt8();
+						event.thirtyseconds = stream.readInt8();
+						return event;
+					case 0x59:
+						event.subtype = 'keySignature';
+						if (length != 2) throw "Expected length for keySignature event is 2, got " + length;
+						event.key = stream.readInt8(true);
+						event.scale = stream.readInt8();
+						return event;
+					case 0x7f:
+						event.subtype = 'sequencerSpecific';
+						event.data = stream.read(length);
+						return event;
+					default:
+						// console.log("Unrecognised meta event subtype: " + subtypeByte);
+						event.subtype = 'unknown'
+						event.data = stream.read(length);
+						return event;
+				}
+				event.data = stream.read(length);
+				return event;
+			} else if (eventTypeByte == 0xf0) {
+				event.type = 'sysEx';
+				var length = stream.readVarInt();
+				event.data = stream.read(length);
+				return event;
+			} else if (eventTypeByte == 0xf7) {
+				event.type = 'dividedSysEx';
+				var length = stream.readVarInt();
+				event.data = stream.read(length);
+				return event;
+			} else {
+				throw "Unrecognised MIDI event type byte: " + eventTypeByte;
+			}
+		} else {
+			/* channel event */
+			var param1;
+			if ((eventTypeByte & 0x80) == 0) {
+				/* running status - reuse lastEventTypeByte as the event type.
+					eventTypeByte is actually the first parameter
+				*/
+				param1 = eventTypeByte;
+				eventTypeByte = lastEventTypeByte;
+			} else {
+				param1 = stream.readInt8();
+				lastEventTypeByte = eventTypeByte;
+			}
+			var eventType = eventTypeByte >> 4;
+			event.channel = eventTypeByte & 0x0f;
+			event.type = 'channel';
+			switch (eventType) {
+				case 0x08:
+					event.subtype = 'noteOff';
+					event.noteNumber = param1;
+					event.velocity = stream.readInt8();
+					return event;
+				case 0x09:
+					event.noteNumber = param1;
+					event.velocity = stream.readInt8();
+					if (event.velocity == 0) {
+						event.subtype = 'noteOff';
+					} else {
+						event.subtype = 'noteOn';
+					}
+					return event;
+				case 0x0a:
+					event.subtype = 'noteAftertouch';
+					event.noteNumber = param1;
+					event.amount = stream.readInt8();
+					return event;
+				case 0x0b:
+					event.subtype = 'controller';
+					event.controllerType = param1;
+					event.value = stream.readInt8();
+					return event;
+				case 0x0c:
+					event.subtype = 'programChange';
+					event.programNumber = param1;
+					return event;
+				case 0x0d:
+					event.subtype = 'channelAftertouch';
+					event.amount = param1;
+					return event;
+				case 0x0e:
+					event.subtype = 'pitchBend';
+					event.value = param1 + (stream.readInt8() << 7);
+					return event;
+				default:
+					throw "Unrecognised MIDI event type: " + eventType
+					/* 
+					console.log("Unrecognised MIDI event type: " + eventType);
+					stream.readInt8();
+					event.subtype = 'unknown';
+					return event;
+					*/
+			}
+		}
+	}
+	
+	stream = Stream(data);
+	var headerChunk = readChunk(stream);
+	if (headerChunk.id != 'MThd' || headerChunk.length != 6) {
+		throw "Bad .mid file - header not found";
+	}
+	var headerStream = Stream(headerChunk.data);
+	var formatType = headerStream.readInt16();
+	var trackCount = headerStream.readInt16();
+	var timeDivision = headerStream.readInt16();
+	
+	if (timeDivision & 0x8000) {
+		throw "Expressing time division in SMTPE frames is not supported yet"
+	} else {
+		ticksPerBeat = timeDivision;
+	}
+	
+	var header = {
+		'formatType': formatType,
+		'trackCount': trackCount,
+		'ticksPerBeat': ticksPerBeat
+	}
+	var tracks = [];
+	for (var i = 0; i < header.trackCount; i++) {
+		tracks[i] = [];
+		var trackChunk = readChunk(stream);
+		if (trackChunk.id != 'MTrk') {
+			throw "Unexpected chunk - expected MTrk, got "+ trackChunk.id;
+		}
+		var trackStream = Stream(trackChunk.data);
+		while (!trackStream.eof()) {
+			var event = readEvent(trackStream);
+			tracks[i].push(event);
+			//console.log(event);
+		}
+	}
+	
+	return {
+		'header': header,
+		'tracks': tracks
+	}
+}

+ 102 - 0
inc/jasmid/replayer.js

@@ -0,0 +1,102 @@
+var clone = function (o) {
+	if (typeof o != 'object') return (o);
+	if (o == null) return (o);
+	var ret = (typeof o.length == 'number') ? [] : {};
+	for (var key in o) ret[key] = clone(o[key]);
+	return ret;
+};
+
+function Replayer(midiFile, timeWarp, eventProcessor, bpm) {
+	var trackStates = [];
+	var beatsPerMinute = bpm ? bpm : 120;
+	var bpmOverride = bpm ? true : false;
+
+	var ticksPerBeat = midiFile.header.ticksPerBeat;
+	
+	for (var i = 0; i < midiFile.tracks.length; i++) {
+		trackStates[i] = {
+			'nextEventIndex': 0,
+			'ticksToNextEvent': (
+				midiFile.tracks[i].length ?
+					midiFile.tracks[i][0].deltaTime :
+					null
+			)
+		};
+	}
+
+	var nextEventInfo;
+	var samplesToNextEvent = 0;
+	
+	function getNextEvent() {
+		var ticksToNextEvent = null;
+		var nextEventTrack = null;
+		var nextEventIndex = null;
+		
+		for (var i = 0; i < trackStates.length; i++) {
+			if (
+				trackStates[i].ticksToNextEvent != null
+				&& (ticksToNextEvent == null || trackStates[i].ticksToNextEvent < ticksToNextEvent)
+			) {
+				ticksToNextEvent = trackStates[i].ticksToNextEvent;
+				nextEventTrack = i;
+				nextEventIndex = trackStates[i].nextEventIndex;
+			}
+		}
+		if (nextEventTrack != null) {
+			/* consume event from that track */
+			var nextEvent = midiFile.tracks[nextEventTrack][nextEventIndex];
+			if (midiFile.tracks[nextEventTrack][nextEventIndex + 1]) {
+				trackStates[nextEventTrack].ticksToNextEvent += midiFile.tracks[nextEventTrack][nextEventIndex + 1].deltaTime;
+			} else {
+				trackStates[nextEventTrack].ticksToNextEvent = null;
+			}
+			trackStates[nextEventTrack].nextEventIndex += 1;
+			/* advance timings on all tracks by ticksToNextEvent */
+			for (var i = 0; i < trackStates.length; i++) {
+				if (trackStates[i].ticksToNextEvent != null) {
+					trackStates[i].ticksToNextEvent -= ticksToNextEvent
+				}
+			}
+			return {
+				"ticksToEvent": ticksToNextEvent,
+				"event": nextEvent,
+				"track": nextEventTrack
+			}
+		} else {
+			return null;
+		}
+	};
+	//
+	var midiEvent;
+	var temporal = [];
+	//
+	function processEvents() {
+		function processNext() {
+		    if (!bpmOverride && midiEvent.event.type == "meta" && midiEvent.event.subtype == "setTempo" ) {
+				// tempo change events can occur anywhere in the middle and affect events that follow
+				beatsPerMinute = 60000000 / midiEvent.event.microsecondsPerBeat;
+			}
+			///
+			var beatsToGenerate = 0;
+			var secondsToGenerate = 0;
+			if (midiEvent.ticksToEvent > 0) {
+				beatsToGenerate = midiEvent.ticksToEvent / ticksPerBeat;
+				secondsToGenerate = beatsToGenerate / (beatsPerMinute / 60);
+			}
+			///
+			var time = (secondsToGenerate * 1000 * timeWarp) || 0;
+			temporal.push([ midiEvent, time]);
+			midiEvent = getNextEvent();
+		};
+		///
+		if (midiEvent = getNextEvent()) {
+			while(midiEvent) processNext(true);
+		}
+	};
+	processEvents();
+	return {
+		"getData": function() {
+			return clone(temporal);
+		}
+	};
+};

+ 69 - 0
inc/jasmid/stream.js

@@ -0,0 +1,69 @@
+/* Wrapper for accessing strings through sequential reads */
+function Stream(str) {
+	var position = 0;
+	
+	function read(length) {
+		var result = str.substr(position, length);
+		position += length;
+		return result;
+	}
+	
+	/* read a big-endian 32-bit integer */
+	function readInt32() {
+		var result = (
+			(str.charCodeAt(position) << 24)
+			+ (str.charCodeAt(position + 1) << 16)
+			+ (str.charCodeAt(position + 2) << 8)
+			+ str.charCodeAt(position + 3));
+		position += 4;
+		return result;
+	}
+
+	/* read a big-endian 16-bit integer */
+	function readInt16() {
+		var result = (
+			(str.charCodeAt(position) << 8)
+			+ str.charCodeAt(position + 1));
+		position += 2;
+		return result;
+	}
+	
+	/* read an 8-bit integer */
+	function readInt8(signed) {
+		var result = str.charCodeAt(position);
+		if (signed && result > 127) result -= 256;
+		position += 1;
+		return result;
+	}
+	
+	function eof() {
+		return position >= str.length;
+	}
+	
+	/* read a MIDI-style variable-length integer
+		(big-endian value in groups of 7 bits,
+		with top bit set to signify that another byte follows)
+	*/
+	function readVarInt() {
+		var result = 0;
+		while (true) {
+			var b = readInt8();
+			if (b & 0x80) {
+				result += (b & 0x7f);
+				result <<= 7;
+			} else {
+				/* b is the last byte */
+				return result + b;
+			}
+		}
+	}
+	
+	return {
+		'eof': eof,
+		'read': read,
+		'readInt32': readInt32,
+		'readInt16': readInt16,
+		'readInt8': readInt8,
+		'readVarInt': readVarInt
+	}
+}

+ 61 - 0
inc/shim/Base64.js

@@ -0,0 +1,61 @@
+//https://github.com/davidchambers/Base64.js
+
+;(function () {
+  var object = typeof exports != 'undefined' ? exports : this; // #8: web workers
+  var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+
+  function InvalidCharacterError(message) {
+    this.message = message;
+  }
+  InvalidCharacterError.prototype = new Error;
+  InvalidCharacterError.prototype.name = 'InvalidCharacterError';
+
+  // encoder
+  // [https://gist.github.com/999166] by [https://github.com/nignag]
+  object.btoa || (
+  object.btoa = function (input) {
+    for (
+      // initialize result and counter
+      var block, charCode, idx = 0, map = chars, output = '';
+      // if the next input index does not exist:
+      //   change the mapping table to "="
+      //   check if d has no fractional digits
+      input.charAt(idx | 0) || (map = '=', idx % 1);
+      // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
+      output += map.charAt(63 & block >> 8 - idx % 1 * 8)
+    ) {
+      charCode = input.charCodeAt(idx += 3/4);
+      if (charCode > 0xFF) {
+        throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
+      }
+      block = block << 8 | charCode;
+    }
+    return output;
+  });
+
+  // decoder
+  // [https://gist.github.com/1020396] by [https://github.com/atk]
+  object.atob || (
+  object.atob = function (input) {
+    input = input.replace(/=+$/, '')
+    if (input.length % 4 == 1) {
+      throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
+    }
+    for (
+      // initialize result and counters
+      var bc = 0, bs, buffer, idx = 0, output = '';
+      // get next character
+      buffer = input.charAt(idx++);
+      // character found in table? initialize bit storage and add its ascii value;
+      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
+        // and if not first of each 4 characters,
+        // convert the first 8 bits to one ascii character
+        bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
+    ) {
+      // try to find character in table (0-63, not found => -1)
+      buffer = chars.indexOf(buffer);
+    }
+    return output;
+  });
+
+}());

+ 81 - 0
inc/shim/Base64binary.js

@@ -0,0 +1,81 @@
+/**
+ * @license -------------------------------------------------------------------
+ *   module: Base64Binary
+ *      src: http://blog.danguer.com/2011/10/24/base64-binary-decoding-in-javascript/
+ *  license: Simplified BSD License
+ * -------------------------------------------------------------------
+ * Copyright 2011, Daniel Guerrero. All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     - Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     - Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+var Base64Binary = {
+	_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
+
+	/* will return a  Uint8Array type */
+	decodeArrayBuffer: function(input) {
+		var bytes = Math.ceil( (3*input.length) / 4.0);
+		var ab = new ArrayBuffer(bytes);
+		this.decode(input, ab);
+
+		return ab;
+	},
+
+	decode: function(input, arrayBuffer) {
+		//get last chars to see if are valid
+		var lkey1 = this._keyStr.indexOf(input.charAt(input.length-1));		 
+		var lkey2 = this._keyStr.indexOf(input.charAt(input.length-1));		 
+
+		var bytes = Math.ceil( (3*input.length) / 4.0);
+		if (lkey1 == 64) bytes--; //padding chars, so skip
+		if (lkey2 == 64) bytes--; //padding chars, so skip
+
+		var uarray;
+		var chr1, chr2, chr3;
+		var enc1, enc2, enc3, enc4;
+		var i = 0;
+		var j = 0;
+
+		if (arrayBuffer)
+			uarray = new Uint8Array(arrayBuffer);
+		else
+			uarray = new Uint8Array(bytes);
+
+		input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
+
+		for (i=0; i<bytes; i+=3) {	
+			//get the 3 octects in 4 ascii chars
+			enc1 = this._keyStr.indexOf(input.charAt(j++));
+			enc2 = this._keyStr.indexOf(input.charAt(j++));
+			enc3 = this._keyStr.indexOf(input.charAt(j++));
+			enc4 = this._keyStr.indexOf(input.charAt(j++));
+
+			chr1 = (enc1 << 2) | (enc2 >> 4);
+			chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+			chr3 = ((enc3 & 3) << 6) | enc4;
+
+			uarray[i] = chr1;			
+			if (enc3 != 64) uarray[i+1] = chr2;
+			if (enc4 != 64) uarray[i+2] = chr3;
+		}
+
+		return uarray;	
+	}
+};

+ 111 - 0
inc/shim/WebAudioAPI.js

@@ -0,0 +1,111 @@
+/**
+ * @license -------------------------------------------------------------------
+ *   module: WebAudioShim - Fix naming issues for WebAudioAPI supports
+ *      src: https://github.com/Dinahmoe/webaudioshim
+ *   author: Dinahmoe AB
+ * -------------------------------------------------------------------
+ * Copyright (c) 2012 DinahMoe AB
+ * 
+ * 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.
+ */
+
+window.AudioContext = window.AudioContext || window.webkitAudioContext || null;
+window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext || null;
+
+(function (Context) {
+	var isFunction = function (f) {
+		return Object.prototype.toString.call(f) === "[object Function]" ||
+			Object.prototype.toString.call(f) === "[object AudioContextConstructor]";
+	};
+	var contextMethods = [
+		["createGainNode", "createGain"],
+		["createDelayNode", "createDelay"],
+		["createJavaScriptNode", "createScriptProcessor"]
+	];
+	///
+	var proto;
+	var instance;
+	var sourceProto;
+	///
+	if (!isFunction(Context)) {
+		return;
+	}
+	instance = new Context();
+	if (!instance.destination || !instance.sampleRate) {
+		return;
+	}
+	proto = Context.prototype;
+	sourceProto = Object.getPrototypeOf(instance.createBufferSource());
+
+	if (!isFunction(sourceProto.start)) {
+		if (isFunction(sourceProto.noteOn)) {
+			sourceProto.start = function (when, offset, duration) {
+				switch (arguments.length) {
+					case 0:
+						throw new Error("Not enough arguments.");
+					case 1:
+						this.noteOn(when);
+						break;
+					case 2:
+						if (this.buffer) {
+							this.noteGrainOn(when, offset, this.buffer.duration - offset);
+						} else {
+							throw new Error("Missing AudioBuffer");
+						}
+						break;
+					case 3:
+						this.noteGrainOn(when, offset, duration);
+				}
+			};
+		}
+	}
+
+	if (!isFunction(sourceProto.noteOn)) {
+		sourceProto.noteOn = sourceProto.start;
+	}
+
+	if (!isFunction(sourceProto.noteGrainOn)) {
+		sourceProto.noteGrainOn = sourceProto.start;
+	}
+
+	if (!isFunction(sourceProto.stop)) {
+		sourceProto.stop = sourceProto.noteOff;
+	}
+
+	if (!isFunction(sourceProto.noteOff)) {
+		sourceProto.noteOff = sourceProto.stop;
+	}
+
+	contextMethods.forEach(function (names) {
+		var name1;
+		var name2;
+		while (names.length) {
+			name1 = names.pop();
+			if (isFunction(this[name1])) {
+				this[names.pop()] = this[name1];
+			} else {
+				name2 = names.pop();
+				this[name1] = this[name2];
+			}
+		}
+	}, proto);
+})(window.AudioContext);

+ 421 - 0
inc/shim/WebMIDIAPI.js

@@ -0,0 +1,421 @@
+/* Copyright 2013 Chris Wilson
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+// Initialize the MIDI library.
+(function (global) {
+    'use strict';
+    var midiIO, _requestMIDIAccess, MIDIAccess, _onReady, MIDIPort, MIDIInput, MIDIOutput, _midiProc;
+
+    function Promise() {
+
+    }
+
+    Promise.prototype.then = function(accept, reject) {
+        this.accept = accept; 
+        this.reject = reject;
+    }
+
+    Promise.prototype.succeed = function(access) {
+        if (this.accept)
+            this.accept(access);
+    }
+
+    Promise.prototype.fail = function(error) {
+        if (this.reject)
+            this.reject(error);
+    }
+
+    function _JazzInstance() {
+        this.inputInUse = false;
+        this.outputInUse = false;
+
+        // load the Jazz plugin
+        var o1 = document.createElement("object");
+        o1.id = "_Jazz" + Math.random() + "ie";
+        o1.classid = "CLSID:1ACE1618-1C7D-4561-AEE1-34842AA85E90";
+
+        this.activeX = o1;
+
+        var o2 = document.createElement("object");
+        o2.id = "_Jazz" + Math.random();
+        o2.type="audio/x-jazz";
+        o1.appendChild(o2);
+
+        this.objRef = o2;
+
+        var e = document.createElement("p");
+        e.appendChild(document.createTextNode("This page requires the "));
+
+        var a = document.createElement("a");
+        a.appendChild(document.createTextNode("Jazz plugin"));
+        a.href = "http://jazz-soft.net/";
+
+        e.appendChild(a);
+        e.appendChild(document.createTextNode("."));
+        o2.appendChild(e);
+
+        var insertionPoint = document.getElementById("MIDIPlugin");
+        if (!insertionPoint) {
+            // Create hidden element
+            var insertionPoint = document.createElement("div");
+            insertionPoint.id = "MIDIPlugin";
+            insertionPoint.style.position = "absolute";
+            insertionPoint.style.visibility = "hidden";
+            insertionPoint.style.left = "-9999px";
+            insertionPoint.style.top = "-9999px";
+            document.body.appendChild(insertionPoint);
+        }
+        insertionPoint.appendChild(o1);
+
+        if (this.objRef.isJazz)
+            this._Jazz = this.objRef;
+        else if (this.activeX.isJazz)
+            this._Jazz = this.activeX;
+        else
+            this._Jazz = null;
+        if (this._Jazz) {
+            this._Jazz._jazzTimeZero = this._Jazz.Time();
+            this._Jazz._perfTimeZero = window.performance.now();
+        }
+    }
+
+    _requestMIDIAccess = function _requestMIDIAccess() {
+        var access = new MIDIAccess();
+        return access._promise;
+    };
+
+    // API Methods
+
+    MIDIAccess = function() {
+        this._jazzInstances = new Array();
+        this._jazzInstances.push( new _JazzInstance() );
+        this._promise = new Promise;
+
+        if (this._jazzInstances[0]._Jazz) {
+            this._Jazz = this._jazzInstances[0]._Jazz;
+            window.setTimeout( _onReady.bind(this), 3 );
+        } else {
+            window.setTimeout( _onNotReady.bind(this), 3 );
+        }
+    };
+
+    _onReady = function _onReady() {
+        if (this._promise)
+            this._promise.succeed(this);
+    };
+
+    function _onNotReady() {
+        if (this._promise)
+            this._promise.fail( { code: 1 } );
+    };
+
+    MIDIAccess.prototype.inputs = function(  ) {
+        if (!this._Jazz)
+              return null;
+        var list=this._Jazz.MidiInList();
+        var inputs = new Array( list.length );
+
+        for ( var i=0; i<list.length; i++ ) {
+            inputs[i] = new MIDIInput( this, list[i], i );
+        }
+        return inputs;
+    }
+
+    MIDIAccess.prototype.outputs = function(  ) {
+        if (!this._Jazz)
+            return null;
+        var list=this._Jazz.MidiOutList();
+        var outputs = new Array( list.length );
+
+        for ( var i=0; i<list.length; i++ ) {
+            outputs[i] = new MIDIOutput( this, list[i], i );
+        }
+        return outputs;
+    };
+
+    MIDIInput = function MIDIInput( midiAccess, name, index ) {
+        this._listeners = [];
+        this._midiAccess = midiAccess;
+        this._index = index;
+        this._inLongSysexMessage = false;
+        this._sysexBuffer = new Uint8Array();
+        this.id = "" + index + "." + name;
+        this.manufacturer = "";
+        this.name = name;
+        this.type = "input";
+        this.version = "";
+        this.onmidimessage = null;
+
+        var inputInstance = null;
+        for (var i=0; (i<midiAccess._jazzInstances.length)&&(!inputInstance); i++) {
+            if (!midiAccess._jazzInstances[i].inputInUse)
+                inputInstance=midiAccess._jazzInstances[i];
+        }
+        if (!inputInstance) {
+            inputInstance = new _JazzInstance();
+            midiAccess._jazzInstances.push( inputInstance );
+        }
+        inputInstance.inputInUse = true;
+
+        this._jazzInstance = inputInstance._Jazz;
+        this._input = this._jazzInstance.MidiInOpen( this._index, _midiProc.bind(this) );
+    };
+
+    // Introduced in DOM Level 2:
+    MIDIInput.prototype.addEventListener = function (type, listener, useCapture ) {
+        if (type !== "midimessage")
+            return;
+        for (var i=0; i<this._listeners.length; i++)
+            if (this._listeners[i] == listener)
+                return;
+        this._listeners.push( listener );
+    };
+
+    MIDIInput.prototype.removeEventListener = function (type, listener, useCapture ) {
+        if (type !== "midimessage")
+            return;
+        for (var i=0; i<this._listeners.length; i++)
+            if (this._listeners[i] == listener) {
+                this._listeners.splice( i, 1 );  //remove it
+                return;
+            }
+    };
+
+    MIDIInput.prototype.preventDefault = function() {
+        this._pvtDef = true;
+    };
+
+    MIDIInput.prototype.dispatchEvent = function (evt) {
+        this._pvtDef = false;
+
+        // dispatch to listeners
+        for (var i=0; i<this._listeners.length; i++)
+            if (this._listeners[i].handleEvent)
+                this._listeners[i].handleEvent.bind(this)( evt );
+            else
+                this._listeners[i].bind(this)( evt );
+
+        if (this.onmidimessage)
+            this.onmidimessage( evt );
+
+        return this._pvtDef;
+    };
+
+    MIDIInput.prototype.appendToSysexBuffer = function ( data ) {
+        var oldLength = this._sysexBuffer.length;
+        var tmpBuffer = new Uint8Array( oldLength + data.length );
+        tmpBuffer.set( this._sysexBuffer );
+        tmpBuffer.set( data, oldLength );
+        this._sysexBuffer = tmpBuffer;
+    };
+
+    MIDIInput.prototype.bufferLongSysex = function ( data, initialOffset ) {
+        var j = initialOffset;
+        while (j<data.length) {
+            if (data[j] == 0xF7) {
+                // end of sysex!
+                j++;
+                this.appendToSysexBuffer( data.slice(initialOffset, j) );
+                return j;
+            }
+            j++;
+        }
+        // didn't reach the end; just tack it on.
+        this.appendToSysexBuffer( data.slice(initialOffset, j) );
+        this._inLongSysexMessage = true;
+        return j;
+    };
+
+    _midiProc = function _midiProc( timestamp, data ) {
+        // Have to use createEvent/initEvent because IE10 fails on new CustomEvent.  Thanks, IE!
+        var length = 0;
+        var i,j;
+        var isSysexMessage = false;
+
+        // Jazz sometimes passes us multiple messages at once, so we need to parse them out
+        // and pass them one at a time.
+
+        for (i=0; i<data.length; i+=length) {
+            if (this._inLongSysexMessage) {
+                i = this.bufferLongSysex(data,i);
+                if ( data[i-1] != 0xf7 ) {
+                    // ran off the end without hitting the end of the sysex message
+                    return;
+                }
+                isSysexMessage = true;
+            } else {
+                isSysexMessage = false;
+                switch (data[i] & 0xF0) {
+                    case 0x80:  // note off
+                    case 0x90:  // note on
+                    case 0xA0:  // polyphonic aftertouch
+                    case 0xB0:  // control change
+                    case 0xE0:  // channel mode
+                        length = 3;
+                        break;
+
+                    case 0xC0:  // program change
+                    case 0xD0:  // channel aftertouch
+                        length = 2;
+                        break;
+
+                    case 0xF0:
+                        switch (data[i]) {
+                            case 0xf0:  // variable-length sysex.
+                                i = this.bufferLongSysex(data,i);
+                                if ( data[i-1] != 0xf7 ) {
+                                    // ran off the end without hitting the end of the sysex message
+                                    return;
+                                }
+                                isSysexMessage = true;
+                                break;
+
+                            case 0xF1:  // MTC quarter frame
+                            case 0xF3:  // song select
+                                length = 2;
+                                break;
+
+                            case 0xF2:  // song position pointer
+                                length = 3;
+                                break;
+
+                            default:
+                                length = 1;
+                                break;
+                        }
+                        break;
+                }
+            }
+            var evt = document.createEvent( "Event" );
+            evt.initEvent( "midimessage", false, false );
+            evt.receivedTime = parseFloat( timestamp.toString()) + this._jazzInstance._perfTimeZero;
+            if (isSysexMessage || this._inLongSysexMessage) {
+                evt.data = new Uint8Array( this._sysexBuffer );
+                this._sysexBuffer = new Uint8Array(0);
+                this._inLongSysexMessage = false;
+            } else
+                evt.data = new Uint8Array(data.slice(i, length+i));
+            this.dispatchEvent( evt );
+        }
+    };
+
+    MIDIOutput = function MIDIOutput( midiAccess, name, index ) {
+        this._listeners = [];
+        this._midiAccess = midiAccess;
+        this._index = index;
+        this.id = "" + index + "." + name;
+        this.manufacturer = "";
+        this.name = name;
+        this.type = "output";
+        this.version = "";
+
+        var outputInstance = null;
+        for (var i=0; (i<midiAccess._jazzInstances.length)&&(!outputInstance); i++) {
+            if (!midiAccess._jazzInstances[i].outputInUse)
+                outputInstance=midiAccess._jazzInstances[i];
+        }
+        if (!outputInstance) {
+            outputInstance = new _JazzInstance();
+            midiAccess._jazzInstances.push( outputInstance );
+        }
+        outputInstance.outputInUse = true;
+
+        this._jazzInstance = outputInstance._Jazz;
+        this._jazzInstance.MidiOutOpen(this.name);
+    };
+
+    function _sendLater() {
+        this.jazz.MidiOutLong( this.data );    // handle send as sysex
+    }
+
+    MIDIOutput.prototype.send = function( data, timestamp ) {
+        var delayBeforeSend = 0;
+        if (data.length === 0)
+            return false;
+
+        if (timestamp)
+            delayBeforeSend = Math.floor( timestamp - window.performance.now() );
+
+        if (timestamp && (delayBeforeSend>1)) {
+            var sendObj = new Object();
+            sendObj.jazz = this._jazzInstance;
+            sendObj.data = data;
+
+            window.setTimeout( _sendLater.bind(sendObj), delayBeforeSend );
+        } else {
+            this._jazzInstance.MidiOutLong( data );
+        }
+        return true;
+    };
+
+    //init: create plugin
+    if (!window.navigator.requestMIDIAccess)
+        window.navigator.requestMIDIAccess = _requestMIDIAccess;
+
+}(window));
+
+// Polyfill window.performance.now() if necessary.
+(function (exports) {
+    var perf = {}, props;
+
+    function findAlt() {
+        var prefix = ['moz', 'webkit', 'o', 'ms'],
+        i = prefix.length,
+            //worst case, we use Date.now()
+            props = {
+                value: (function (start) {
+                    return function () {
+                        return Date.now() - start;
+                    };
+                }(Date.now()))
+            };
+
+        //seach for vendor prefixed version
+        for (; i >= 0; i--) {
+            if ((prefix[i] + "Now") in exports.performance) {
+                props.value = function (method) {
+                    return function () {
+                        exports.performance[method]();
+                    }
+                }(prefix[i] + "Now");
+                return props;
+            }
+        }
+
+        //otherwise, try to use connectionStart
+        if ("timing" in exports.performance && "connectStart" in exports.performance.timing) {
+            //this pretty much approximates performance.now() to the millisecond
+            props.value = (function (start) {
+                return function() {
+                    Date.now() - start;
+                };
+            }(exports.performance.timing.connectStart));
+        }
+        return props;
+    }
+
+    //if already defined, bail
+    if (("performance" in exports) && ("now" in exports.performance))
+        return;
+    if (!("performance" in exports))
+        Object.defineProperty(exports, "performance", {
+            get: function () {
+                return perf;
+            }});
+        //otherwise, performance is there, but not "now()"
+
+    props = findAlt();
+    Object.defineProperty(exports.performance, "now", props);
+}(window));

+ 179 - 0
inc/tuna/README.md

@@ -0,0 +1,179 @@
+Latest news
+====
+New effects added: Chorus, Phaser, and Tremolo. <br /> Be sure to follow us at <a href="https://twitter.com/DinahmoeSTHLM">@DinahmoeSTHLM</a> for future updates. Feel free to create your own effects and give us a pull request!
+
+tuna
+====
+
+An audio effects library for the Web Audio API. Created by <a href="http://www.dinahmoe.com">DinahMoe</a>
+
+<img src="https://i.chzbgr.com/completestore/12/9/4/rjttPiC7WE6S4Bi22aYp1A2.jpg" alt="tuna, tuna, tuna"/>
+
+Effect list:
+====
+<ul>
+    <li>Overdrive (6 different algorithms)</li>
+    <li>Filter</li>
+    <li>Cabinet</li>
+    <li>Delay</li>
+    <li>Convolver (Reverb)</li>
+    <li>Compressor</li>
+    <li>WahWah</li>
+    <li>Tremolo</li>
+    <li>Phaser</li>
+    <li>Chorus</li>
+</ul>
+
+Usage
+====
+
+Start by creating a new Tuna object like so:
+
+<pre>
+var context = new webkitAudioContext();
+var tuna = new Tuna(context);
+</pre>
+
+You need to pass the audio context you're using in your application. Tuna will be using it to create its effects.
+
+You create a new tuna node as such:
+
+<pre>
+var chorus = new tuna.Chorus({
+                 rate: 1.5,
+                 feedback: 0.2,
+                 delay: 0.0045,
+                 bypass: 0
+             });
+</pre>
+You can then connect the tuna node to native Web Audio nodes by doing:
+<pre>
+nativeNode.connect(chorus.input);
+chorus.connect(anotherNativeNode);
+</pre>
+or to other tuna nodes by doing:
+<pre>
+tunaNode.connect(chorus.input);
+chorus.connect(anotherTunaNode.input);
+</pre>
+All tuna nodes are connected TO by using the nodes input property, but connecting FROM the tuna node works as it does with ordinary native AudioNodes.
+
+
+The nodes
+====
+
+A basic chorus effect.
+<pre>
+var chorus = new tuna.Chorus({
+                 rate: 1.5,         //0.01 to 8+
+                 feedback: 0.2,     //0 to 1+
+                 delay: 0.0045,     //0 to 1
+                 bypass: 0          //the value 1 starts the effect as bypassed, 0 or 1
+             });
+</pre>
+
+A delay effect with feedback and a highpass filter applied to the delayed signal.
+<pre>
+var delay = new tuna.Delay({
+                feedback: 0.45,    //0 to 1+
+                delayTime: 150,    //how many milliseconds should the wet signal be delayed? 
+                wetLevel: 0.25,    //0 to 1+
+                dryLevel: 1,       //0 to 1+
+                cutoff: 20,        //cutoff frequency of the built in highpass-filter. 20 to 22050
+                bypass: 0
+            });
+</pre>
+
+A basic phaser effect.
+<pre>
+var phaser = new tuna.Phaser({
+                 rate: 1.2,                     //0.01 to 8 is a decent range, but higher values are possible
+                 depth: 0.3,                    //0 to 1
+                 feedback: 0.2,                 //0 to 1+
+                 stereoPhase: 30,               //0 to 180
+                 baseModulationFrequency: 700,  //500 to 1500
+                 bypass: 0
+             });
+</pre>
+
+A basic overdrive effect.
+<pre>
+var overdrive = new tuna.Overdrive({
+                    outputGain: 0.5,         //0 to 1+
+                    drive: 0.7,              //0 to 1
+                    curveAmount: 1,          //0 to 1
+                    algorithmIndex: 0,       //0 to 5, selects one of our drive algorithms
+                    bypass: 0
+                });
+</pre>
+
+A compressor with the option to use automatic makeup gain.
+<pre>
+var compressor = new tuna.Compressor({
+                     threshold: 0.5,    //-100 to 0
+                     makeupGain: 1,     //0 and up
+                     attack: 1,         //0 to 1000
+                     release: 0,        //0 to 3000
+                     ratio: 4,          //1 to 20
+                     knee: 5,           //0 to 40
+                     automakeup: true,  //true/false
+                     bypass: 0
+                 });
+</pre>
+
+A convolver with high- and lowcut. You can find a lot of impulse resonses <a href="http://chromium.googlecode.com/svn/trunk/samples/audio/impulse-responses/">here</a>
+<pre>
+var convolver = new tuna.Convolver({
+                    highCut: 22050,                         //20 to 22050
+                    lowCut: 20,                             //20 to 22050
+                    dryLevel: 1,                            //0 to 1+
+                    wetLevel: 1,                            //0 to 1+
+                    level: 1,                               //0 to 1+, adjusts total output of both wet and dry
+                    impulse: "impulses/impulse_rev.wav",    //the path to your impulse response
+                    bypass: 0
+                });
+</pre>
+
+A basic filter.
+<pre>
+var filter = new tuna.Filter({
+                 frequency: 20,         //20 to 22050
+                 Q: 1,                  //0.001 to 100
+                 gain: 0,               //-40 to 40
+                 bypass: 1,             //0 to 1+
+                 filterType: 0,         //0 to 7, corresponds to the filter types in the native filter node: lowpass, highpass, bandpass, lowshelf, highshelf, peaking, notch, allpass in that order
+                 bypass: 0
+             });
+</pre>
+
+A cabinet/speaker emulator.
+<pre>
+var cabinet = new tuna.Cabinet({
+                  makeupGain: 1,                                 //0 to 20
+                  impulsePath: "impulses/impulse_guitar.wav",    //path to your speaker impulse
+                  bypass: 0
+              });
+</pre>
+
+A basic tremolo.
+<pre>
+var tremolo = new tuna.Tremolo({
+                  intensity: 0.3,    //0 to 1
+                  rate: 0.1,         //0.001 to 8
+                  stereoPhase: 0,    //0 to 180
+                  bypass: 0
+              });
+</pre>
+
+A wahwah with an auto wah option.
+<pre>
+var wahwah = new tuna.WahWah({
+                 automode: true,                //true/false
+                 baseFrequency: 0.5,            //0 to 1
+                 excursionOctaves: 2,           //1 to 6
+                 sweep: 0.2,                    //0 to 1
+                 resonance: 10,                 //1 to 100
+                 sensitivity: 0.5,              //-1 to 1
+                 bypass: 0
+             });
+</pre>

BIN
inc/tuna/impulses/Sweetspot1M.wav


BIN
inc/tuna/impulses/impulse_guitar.wav


BIN
inc/tuna/impulses/impulse_rev.wav


BIN
inc/tuna/impulses/ir_rev_short.wav


+ 2093 - 0
inc/tuna/tuna.js

@@ -0,0 +1,2093 @@
+/*
+    Copyright (c) 2012 DinahMoe AB
+
+    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.
+    
+    Bitcrusher & Moog Filter by Zach Denton
+*/
+
+// https://developer.mozilla.org/en-US/docs/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
+
+// Originally written by Alessandro Saccoia, Chris Coniglio and Oskar Eriksson
+
+(function (window) {
+    var userContext;
+    var userInstance;
+    var Tuna = function (context) {
+			if (!window.AudioContext) {
+				window.AudioContext = window.webkitAudioContext;
+			}
+            if (!context) {
+                console.log("tuna.js: Missing audio context! Creating a new context for you.");
+                context = window.AudioContext && (new window.AudioContext());
+            }
+            userContext = context;
+            userInstance = this;
+        },
+        version = "0.1",
+        set = "setValueAtTime",
+        linear = "linearRampToValueAtTime",
+        pipe = function (param, val) {
+            param.value = val;
+        },
+        Super = Object.create(null, {
+            activate: {
+                writable: true,
+                value: function (doActivate) {
+                    if (doActivate) {
+                        this.input.disconnect();
+                        this.input.connect(this.activateNode);
+                        if (this.activateCallback) {
+                            this.activateCallback(doActivate);
+                        }
+                    } else {
+                        this.input.disconnect();
+                        this.input.connect(this.output);
+                    }
+                }
+            },
+            bypass: {
+                get: function () {
+                    return this._bypass;
+                },
+                set: function (value) {
+                    if (this._lastBypassValue === value) {
+                        return;
+                    }
+                    this._bypass = value;
+                    this.activate(!value);
+                    this._lastBypassValue = value;
+                }
+            },
+            connect: {
+                value: function (target) {
+                    this.output.connect(target);
+                }
+            },
+            disconnect: {
+                value: function (target) {
+                    this.output.disconnect(target);
+                }
+            },
+            connectInOrder: {
+                value: function (nodeArray) {
+                    var i = nodeArray.length - 1;
+                    while(i--) {
+                        if (!nodeArray[i].connect) {
+                            return console.error("AudioNode.connectInOrder: TypeError: Not an AudioNode.", nodeArray[i]);
+                        }
+                        if (nodeArray[i + 1].input) {
+                            nodeArray[i].connect(nodeArray[i + 1].input);
+                        } else {
+                            nodeArray[i].connect(nodeArray[i + 1]);
+                        }
+                    }
+                }
+            },
+            getDefaults: {
+                value: function () {
+                    var result = {};
+                    for(var key in this.defaults) {
+                        result[key] = this.defaults[key].value;
+                    }
+                    return result;
+                }
+            },
+            getValues: {
+                value: function () {
+                    var result = {};
+                    for(var key in this.defaults) {
+                        result[key] = this[key];
+                    }
+                    return result;
+                }
+            },
+            automate: {
+                value: function (property, value, duration, startTime) {
+                    var start = startTime ? ~~ (startTime / 1000) : userContext.currentTime,
+                        dur = duration ? ~~ (duration / 1000) : 0,
+                        _is = this.defaults[property],
+                        param = this[property],
+                        method;
+
+                    if (param) {
+                        if (_is.automatable) {
+                            if (!duration) {
+                                method = set;
+                            } else {
+                                method = linear;
+                                param.cancelScheduledValues(start);
+                                param.setValueAtTime(param.value, start);
+                            }
+                            param[method](value, dur + start);
+                        } else {
+                            param = value;
+                        }
+                    } else {
+                        console.error("Invalid Property for " + this.name);
+                    }
+                }
+            }
+        }),
+        FLOAT = "float",
+        BOOLEAN = "boolean",
+        STRING = "string",
+        INT = "int";
+
+    function dbToWAVolume(db) {
+        return Math.max(0, Math.round(100 * Math.pow(2, db / 6)) / 100);
+    }
+
+    function fmod(x, y) {
+        // http://kevin.vanzonneveld.net
+        // +   original by: Onno Marsman
+        // +      input by: Brett Zamir (http://brett-zamir.me)
+        // +   bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+        // *     example 1: fmod(5.7, 1.3);
+        // *     returns 1: 0.5
+        var tmp, tmp2, p = 0,
+            pY = 0,
+            l = 0.0,
+            l2 = 0.0;
+
+        tmp = x.toExponential().match(/^.\.?(.*)e(.+)$/);
+        p = parseInt(tmp[2], 10) - (tmp[1] + '').length;
+        tmp = y.toExponential().match(/^.\.?(.*)e(.+)$/);
+        pY = parseInt(tmp[2], 10) - (tmp[1] + '').length;
+
+        if (pY > p) {
+            p = pY;
+        }
+
+        tmp2 = (x % y);
+
+        if (p < -100 || p > 20) {
+            // toFixed will give an out of bound error so we fix it like this:
+            l = Math.round(Math.log(tmp2) / Math.log(10));
+            l2 = Math.pow(10, l);
+
+            return(tmp2 / l2).toFixed(l - p) * l2;
+        } else {
+            return parseFloat(tmp2.toFixed(-p));
+        }
+    }
+
+    function sign(x) {
+        if (x === 0) {
+            return 1;
+        } else {
+            return Math.abs(x) / x;
+        }
+    }
+
+    function tanh(n) {
+        return(Math.exp(n) - Math.exp(-n)) / (Math.exp(n) + Math.exp(-n));
+    }
+
+    Tuna.toString = Tuna.prototype.toString = function () {
+        return "You are running Tuna version " + version + " by Dinahmoe!";
+    };
+    Tuna.prototype.Filter = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.filter = userContext.createBiquadFilter();
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.filter);
+        this.filter.connect(this.output);
+
+        this.frequency = properties.frequency || this.defaults.frequency.value;
+        this.Q = properties.resonance || this.defaults.Q.value;
+        this.filterType = properties.filterType || this.defaults.filterType.value;
+        this.gain = properties.gain || this.defaults.gain.value;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Filter.prototype = Object.create(Super, {
+        name: {
+            value: "Filter"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                frequency: {
+                    value: 800,
+                    min: 20,
+                    max: 22050,
+                    automatable: true
+                },
+                Q: {
+                    value: 1,
+                    min: 0.001,
+                    max: 100,
+                    automatable: true
+                },
+                gain: {
+                    value: 0,
+                    min: -40,
+                    max: 40,
+                    automatable: true
+                },
+                bypass: {
+                    value: true,
+                    automatable: false,
+                    type: BOOLEAN
+                },
+                filterType: {
+                    value: 1,
+                    min: 0,
+                    max: 7,
+                    automatable: false,
+                    type: INT
+                }
+            }
+        },
+        filterType: {
+            enumerable: true,
+            get: function () {
+                return this.filter.type;
+            },
+            set: function (value) {
+                this.filter.type = value;
+            }
+        },
+        Q: {
+            enumerable: true,
+            get: function () {
+                return this.filter.Q;
+            },
+            set: function (value) {
+                this.filter.Q.value = value;
+            }
+        },
+        gain: {
+            enumerable: true,
+            get: function () {
+                return this.filter.gain;
+            },
+            set: function (value) {
+                this.filter.gain.value = value;
+            }
+        },
+        frequency: {
+            enumerable: true,
+            get: function () {
+                return this.filter.frequency;
+            },
+            set: function (value) {
+                this.filter.frequency.value = value;
+            }
+        }
+    });
+    Tuna.prototype.Bitcrusher = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.bufferSize = properties.bufferSize || this.defaults.bufferSize.value;
+
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.processor = userContext.createScriptProcessor(this.bufferSize, 1, 1);
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.processor);
+        this.processor.connect(this.output);
+
+        var phaser = 0, last = 0;
+        this.processor.onaudioprocess = function (e) {
+            var input = e.inputBuffer.getChannelData(0),
+                output = e.outputBuffer.getChannelData(0),
+                step = Math.pow(1/2, this.bits);
+            for(var i = 0; i < input.length; i++) {
+                phaser += this.normfreq;
+                if (phaser >= 1.0) {
+                    phaser -= 1.0;
+                    last = step * Math.floor(input[i] / step + 0.5);
+                }
+                output[i] = last;
+            }
+        };
+
+        this.bits = properties.bits || this.defaults.bits.value;
+        this.normfreq = properties.normfreq || this.defaults.normfreq.value;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Bitcrusher.prototype = Object.create(Super, {
+        name: {
+            value: "Bitcrusher"
+        },
+        defaults: {
+            writable: true,
+            value: {
+                bits: {
+                    value: 4,
+                    min: 1,
+                    max: 16,
+                    automatable: false,
+                    type: INT
+                },
+                bufferSize: {
+                    value: 4096,
+                    min: 256,
+                    max: 16384,
+                    automatable: false,
+                    type: INT
+                },
+                bypass: {
+                    value: false,
+                    automatable: false,
+                    type: BOOLEAN
+                },
+                normfreq: {
+                    value: 0.1,
+                    min: 0.0001,
+                    max: 1.0,
+                    automatable: false,
+                }
+            }
+        },
+        bits: {
+            enumerable: true,
+            get: function () {
+                return this.processor.bits;
+            },
+            set: function (value) {
+                this.processor.bits = value;
+            }
+        },
+        normfreq: {
+            enumerable: true,
+            get: function () {
+                return this.processor.normfreq;
+            },
+            set: function (value) {
+                this.processor.normfreq = value;
+            }
+        }
+    });
+    Tuna.prototype.Cabinet = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.convolver = this.newConvolver(properties.impulsePath || "../impulses/impulse_guitar.wav");
+        this.makeupNode = userContext.createGain();
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.convolver.input);
+        this.convolver.output.connect(this.makeupNode);
+        this.makeupNode.connect(this.output);
+
+        this.makeupGain = properties.makeupGain || this.defaults.makeupGain;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Cabinet.prototype = Object.create(Super, {
+        name: {
+            value: "Cabinet"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                makeupGain: {
+                    value: 1,
+                    min: 0,
+                    max: 20,
+                    automatable: true
+                },
+                bypass: {
+                    value: false,
+                    automatable: false,
+                    type: BOOLEAN
+                }
+            }
+        },
+        makeupGain: {
+            enumerable: true,
+            get: function () {
+                return this.makeupNode.gain;
+            },
+            set: function (value) {
+                this.makeupNode.gain.value = value;
+            }
+        },
+        newConvolver: {
+            value: function (impulsePath) {
+                return new userInstance.Convolver({
+                    impulse: impulsePath,
+                    dryLevel: 0,
+                    wetLevel: 1
+                });
+            }
+        }
+    });
+    Tuna.prototype.Chorus = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.attenuator = this.activateNode = userContext.createGain();
+        this.splitter = userContext.createChannelSplitter(2);
+        this.delayL = userContext.createDelayNode();
+        this.delayR = userContext.createDelayNode();
+        this.feedbackGainNodeLR = userContext.createGain();
+        this.feedbackGainNodeRL = userContext.createGain();
+        this.merger = userContext.createChannelMerger(2);
+        this.output = userContext.createGain();
+
+        this.lfoL = new userInstance.LFO({
+            target: this.delayL.delayTime,
+            callback: pipe
+        });
+        this.lfoR = new userInstance.LFO({
+            target: this.delayR.delayTime,
+            callback: pipe
+        });
+
+        this.input.connect(this.attenuator);
+        this.attenuator.connect(this.output);
+        this.attenuator.connect(this.splitter);
+        this.splitter.connect(this.delayL, 0);
+        this.splitter.connect(this.delayR, 1);
+        this.delayL.connect(this.feedbackGainNodeLR);
+        this.delayR.connect(this.feedbackGainNodeRL);
+        this.feedbackGainNodeLR.connect(this.delayR);
+        this.feedbackGainNodeRL.connect(this.delayL);
+        this.delayL.connect(this.merger, 0, 0);
+        this.delayR.connect(this.merger, 0, 1);
+        this.merger.connect(this.output);
+
+        this.feedback = properties.feedback || this.defaults.feedback.value;
+        this.rate = properties.rate || this.defaults.rate.value;
+        this.delay = properties.delay || this.defaults.delay.value;
+        this.depth = properties.depth || this.defaults.depth.value;
+        this.lfoR.phase = Math.PI / 2;
+        this.attenuator.gain.value = 0.6934; // 1 / (10 ^ (((20 * log10(3)) / 3) / 20))
+        this.lfoL.activate(true);
+        this.lfoR.activate(true);
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Chorus.prototype = Object.create(Super, {
+        name: {
+            value: "Chorus"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                feedback: {
+                    value: 0.4,
+                    min: 0,
+                    max: 0.95,
+                    automatable: false,
+                },
+                delay: {
+                    value: 0.0045,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                depth: {
+                    value: 0.7,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                rate: {
+                    value: 1.5,
+                    min: 0,
+                    max: 8,
+                    automatable: false,
+                },
+                bypass: {
+                    value: true,
+                    automatable: false,
+                    type: BOOLEAN
+                }
+            }
+        },
+        delay: {
+            enumerable: true,
+            get: function () {
+                return this._delay;
+            },
+            set: function (value) {
+                this._delay = 0.0002 * (Math.pow(10, value) * 2);
+                this.lfoL.offset = this._delay;
+                this.lfoR.offset = this._delay;
+                this._depth = this._depth;
+            }
+        },
+        depth: {
+            enumerable: true,
+            get: function () {
+                return this._depth;
+            },
+            set: function (value) {
+                this._depth = value;
+                this.lfoL.oscillation = this._depth * this._delay;
+                this.lfoR.oscillation = this._depth * this._delay;
+            }
+        },
+        feedback: {
+            enumerable: true,
+            get: function () {
+                return this._feedback;
+            },
+            set: function (value) {
+                this._feedback = value;
+                this.feedbackGainNodeLR.gain.value = this._feedback;
+                this.feedbackGainNodeRL.gain.value = this._feedback;
+            }
+        },
+        rate: {
+            enumerable: true,
+            get: function () {
+                return this._rate;
+            },
+            set: function (value) {
+                this._rate = value;
+                this.lfoL.frequency = this._rate;
+                this.lfoR.frequency = this._rate;
+            }
+        }
+    });
+    Tuna.prototype.Compressor = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.compNode = this.activateNode = userContext.createDynamicsCompressor();
+        this.makeupNode = userContext.createGain();
+        this.output = userContext.createGain();
+
+        this.compNode.connect(this.makeupNode);
+        this.makeupNode.connect(this.output);
+
+        this.automakeup = properties.automakeup || this.defaults.automakeup.value;
+        this.makeupGain = properties.makeupGain || this.defaults.makeupGain.value;
+        this.threshold = properties.threshold || this.defaults.threshold.value;
+        this.release = properties.release || this.defaults.release.value;
+        this.attack = properties.attack || this.defaults.attack.value;
+        this.ratio = properties.ratio || this.defaults.ratio.value;
+        this.knee = properties.knee || this.defaults.knee.value;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Compressor.prototype = Object.create(Super, {
+        name: {
+            value: "Compressor"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                threshold: {
+                    value: -20,
+                    min: -60,
+                    max: 0,
+                    automatable: true
+                },
+                release: {
+                    value: 250,
+                    min: 10,
+                    max: 2000,
+                    automatable: true
+                },
+                makeupGain: {
+                    value: 1,
+                    min: 1,
+                    max: 100,
+                    automatable: true
+                },
+                attack: {
+                    value: 1,
+                    min: 0,
+                    max: 1000,
+                    automatable: true
+                },
+                ratio: {
+                    value: 4,
+                    min: 1,
+                    max: 50,
+                    automatable: true
+                },
+                knee: {
+                    value: 5,
+                    min: 0,
+                    max: 40,
+                    automatable: true
+                },
+                automakeup: {
+                    value: false,
+                    automatable: false,
+                    type: BOOLEAN
+                },
+                bypass: {
+                    value: true,
+                    automatable: false,
+                    type: BOOLEAN
+                }
+            }
+        },
+        computeMakeup: {
+            value: function () {
+                var magicCoefficient = 4,
+                    // raise me if the output is too hot
+                    c = this.compNode;
+                return -(c.threshold.value - c.threshold.value / c.ratio.value) / magicCoefficient;
+            }
+        },
+        automakeup: {
+            enumerable: true,
+            get: function () {
+                return this._automakeup;
+            },
+            set: function (value) {
+                this._automakeup = value;
+                if (this._automakeup) this.makeupGain = this.computeMakeup();
+            }
+        },
+        threshold: {
+            enumerable: true,
+            get: function () {
+                return this.compNode.threshold;
+            },
+            set: function (value) {
+                this.compNode.threshold.value = value;
+                if (this._automakeup) this.makeupGain = this.computeMakeup();
+            }
+        },
+        ratio: {
+            enumerable: true,
+            get: function () {
+                return this.compNode.ratio;
+            },
+            set: function (value) {
+                this.compNode.ratio.value = value;
+                if (this._automakeup) this.makeupGain = this.computeMakeup();
+            }
+        },
+        knee: {
+            enumerable: true,
+            get: function () {
+                return this.compNode.knee;
+            },
+            set: function (value) {
+                this.compNode.knee.value = value;
+                if (this._automakeup) this.makeupGain = this.computeMakeup();
+            }
+        },
+        attack: {
+            enumerable: true,
+            get: function () {
+                return this.compNode.attack;
+            },
+            set: function (value) {
+                this.compNode.attack.value = value / 1000;
+            }
+        },
+        release: {
+            enumerable: true,
+            get: function () {
+                return this.compNode.release;
+            },
+            set: function (value) {
+                this.compNode.release = value / 1000;
+            }
+        },
+        makeupGain: {
+            enumerable: true,
+            get: function () {
+                return this.makeupNode.gain;
+            },
+            set: function (value) {
+                this.makeupNode.gain.value = dbToWAVolume(value);
+            }
+        }
+    });
+    Tuna.prototype.Convolver = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.convolver = userContext.createConvolver();
+        this.dry = userContext.createGain();
+        this.filterLow = userContext.createBiquadFilter();
+        this.filterHigh = userContext.createBiquadFilter();
+        this.wet = userContext.createGain();
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.filterLow);
+        this.activateNode.connect(this.dry);
+        this.filterLow.connect(this.filterHigh);
+        this.filterHigh.connect(this.convolver);
+        this.convolver.connect(this.wet);
+        this.wet.connect(this.output);
+        this.dry.connect(this.output);
+
+        this.dryLevel = properties.dryLevel || this.defaults.dryLevel.value;
+        this.wetLevel = properties.wetLevel || this.defaults.wetLevel.value;
+        this.highCut = properties.highCut || this.defaults.highCut.value;
+        this.buffer = properties.impulse || "../impulses/ir_rev_short.wav";
+        this.lowCut = properties.lowCut || this.defaults.lowCut.value;
+        this.level = properties.level || this.defaults.level.value;
+        this.filterHigh.type = this.filterHigh.LOWPASS;
+        this.filterLow.type = this.filterHigh.HIGHPASS;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Convolver.prototype = Object.create(Super, {
+        name: {
+            value: "Convolver"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                highCut: {
+                    value: 22050,
+                    min: 20,
+                    max: 22050,
+                    automatable: true
+                },
+                lowCut: {
+                    value: 20,
+                    min: 20,
+                    max: 22050,
+                    automatable: true
+                },
+                dryLevel: {
+                    value: 1,
+                    min: 0,
+                    max: 1,
+                    automatable: true
+                },
+                wetLevel: {
+                    value: 1,
+                    min: 0,
+                    max: 1,
+                    automatable: true
+                },
+                level: {
+                    value: 1,
+                    min: 0,
+                    max: 1,
+                    automatable: true
+                }
+            }
+        },
+        lowCut: {
+            get: function () {
+                return this.filterLow.frequency;
+            },
+            set: function (value) {
+                this.filterLow.frequency.value = value;
+            }
+        },
+        highCut: {
+            get: function () {
+                return this.filterHigh.frequency;
+            },
+            set: function (value) {
+                this.filterHigh.frequency.value = value;
+            }
+        },
+        level: {
+            get: function () {
+                return this.output.gain;
+            },
+            set: function (value) {
+                this.output.gain.value = value;
+            }
+        },
+        dryLevel: {
+            get: function () {
+                return this.dry.gain
+            },
+            set: function (value) {
+                this.dry.gain.value = value;
+            }
+        },
+        wetLevel: {
+            get: function () {
+                return this.wet.gain;
+            },
+            set: function (value) {
+                this.wet.gain.value = value;
+            }
+        },
+        buffer: {
+            enumerable: false,
+            get: function () {
+                return this.convolver.buffer;
+            },
+            set: function (impulse) {
+                var convolver = this.convolver,
+                    xhr = new XMLHttpRequest();
+                if (!impulse) {
+                    console.log("Tuna.Convolver.setBuffer: Missing impulse path!");
+                    return;
+                }
+                xhr.open("GET", impulse, true);
+                xhr.responseType = "arraybuffer";
+                xhr.onreadystatechange = function () {
+                    if (xhr.readyState === 4) {
+                        if (xhr.status < 300 && xhr.status > 199 || xhr.status === 302) {
+                            userContext.decodeAudioData(xhr.response, function (buffer) {
+                                convolver.buffer = buffer;
+                            }, function (e) {
+                                if (e) console.log("Tuna.Convolver.setBuffer: Error decoding data" + e);
+                            });
+                        }
+                    }
+                };
+                xhr.send(null);
+            }
+        }
+    });
+    Tuna.prototype.Delay = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.dry = userContext.createGain();
+        this.wet = userContext.createGain();
+        this.filter = userContext.createBiquadFilter();
+        this.delay = userContext.createDelayNode();
+        this.feedbackNode = userContext.createGain();
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.delay);
+        this.activateNode.connect(this.dry);
+        this.delay.connect(this.filter);
+        this.filter.connect(this.feedbackNode);
+        this.feedbackNode.connect(this.delay);
+        this.feedbackNode.connect(this.wet);
+        this.wet.connect(this.output);
+        this.dry.connect(this.output);
+
+        this.delayTime = properties.delayTime || this.defaults.delayTime.value;
+        this.feedback = properties.feedback || this.defaults.feedback.value;
+        this.wetLevel = properties.wetLevel || this.defaults.wetLevel.value;
+        this.dryLevel = properties.dryLevel || this.defaults.dryLevel.value;
+        this.cutoff = properties.cutoff || this.defaults.cutoff.value;
+        this.filter.type = this.filter.LOWPASS;
+        this.bypass = properties.bypass || false;
+    };
+    
+    Tuna.prototype.Delay.prototype = Object.create(Super, {
+        name: {
+            value: "Delay"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                delayTime: {
+                    value: 100,
+                    min: 20,
+                    max: 1000,
+                    automatable: false,
+                },
+                feedback: {
+                    value: 0.45,
+                    min: 0,
+                    max: 0.9,
+                    automatable: true
+                },
+                cutoff: {
+                    value: 20000,
+                    min: 20,
+                    max: 20000,
+                    automatable: true
+                },
+                wetLevel: {
+                    value: 0.5,
+                    min: 0,
+                    max: 1,
+                    automatable: true
+                },
+                dryLevel: {
+                    value: 1,
+                    min: 0,
+                    max: 1,
+                    automatable: true
+                }
+            }
+        },
+        delayTime: {
+            enumerable: true,
+            get: function () {
+                return this.delay.delayTime;
+            },
+            set: function (value) {
+                this.delay.delayTime.value = value / 1000;
+            }
+        },
+        wetLevel: {
+            enumerable: true,
+            get: function () {
+                return this.wet.gain;
+            },
+            set: function (value) {
+                this.wet.gain.value = value;
+            }
+        },
+        dryLevel: {
+            enumerable: true,
+            get: function () {
+                return this.dry.gain;
+            },
+            set: function (value) {
+                this.dry.gain.value = value;
+            }
+        },
+        feedback: {
+            enumerable: true,
+            get: function () {
+                return this.feedbackNode.gain;
+            },
+            set: function (value) {
+                this.feedbackNode.gain.value = value;
+            }
+        },
+        cutoff: {
+            enumerable: true,
+            get: function () {
+                return this.filter.frequency;
+            },
+            set: function (value) {
+                this.filter.frequency.value = value;
+            }
+        }
+    });
+    Tuna.prototype.MoogFilter = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.bufferSize = properties.bufferSize || this.defaults.bufferSize.value;
+
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.processor = userContext.createScriptProcessor(this.bufferSize, 1, 1);
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.processor);
+        this.processor.connect(this.output);
+
+        var in1, in2, in3, in4, out1, out2, out3, out4;
+        in1 = in2 = in3 = in4 = out1 = out2 = out3 = out4 = 0.0;
+        this.processor.onaudioprocess = function (e) {
+            var input = e.inputBuffer.getChannelData(0),
+                output = e.outputBuffer.getChannelData(0),
+                f = this.cutoff * 1.16,
+                fb = this.resonance * (1.0 - 0.15 * f * f);
+            for(var i = 0; i < input.length; i++) {
+                input[i] -= out4 * fb;
+                input[i] *= 0.35013 * (f*f)*(f*f);
+                out1 = input[i] + 0.3 * in1 + (1 - f) * out1; // Pole 1
+                in1 = input[i];
+                out2 = out1 + 0.3 * in2 + (1 - f) * out2; // Pole 2
+                in2 = out1;
+                out3 = out2 + 0.3 * in3 + (1 - f) * out3; // Pole 3
+                in3 = out2;
+                out4 = out3 + 0.3 * in4 + (1 - f) * out4; // Pole 4
+                in4 = out3;
+                output[i] = out4;
+            }
+        };
+
+        this.cutoff = properties.cutoff || this.defaults.cutoff.value;
+        this.resonance = properties.resonance || this.defaults.resonance.value;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.MoogFilter.prototype = Object.create(Super, {
+        name: {
+            value: "MoogFilter"
+        },
+        defaults: {
+            writable: true,
+            value: {
+                bufferSize: {
+                    value: 4096,
+                    min: 256,
+                    max: 16384,
+                    automatable: false,
+                    type: INT
+                },
+                bypass: {
+                    value: false,
+                    automatable: false,
+                    type: BOOLEAN
+                },
+                cutoff: {
+                    value: 0.065,
+                    min: 0.0001,
+                    max: 1.0,
+                    automatable: false,
+                },
+                resonance: {
+                    value: 3.5,
+                    min: 0.0,
+                    max: 4.0,
+                    automatable: false,
+                }
+            }
+        },
+        cutoff: {
+            enumerable: true,
+            get: function () {
+                return this.processor.cutoff;
+            },
+            set: function (value) {
+                this.processor.cutoff = value;
+            }
+        },
+        resonance: {
+            enumerable: true,
+            get: function () {
+                return this.processor.resonance;
+            },
+            set: function (value) {
+                this.processor.resonance = value;
+            }
+        }
+    });
+    Tuna.prototype.Overdrive = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.inputDrive = userContext.createGain();
+        this.waveshaper = userContext.createWaveShaper();
+        this.outputDrive = userContext.createGain();
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.inputDrive);
+        this.inputDrive.connect(this.waveshaper);
+        this.waveshaper.connect(this.outputDrive);
+        this.outputDrive.connect(this.output);
+
+        this.ws_table = new Float32Array(this.k_nSamples);
+        this.drive = properties.drive || this.defaults.drive.value;
+        this.outputGain = properties.outputGain || this.defaults.outputGain.value;
+        this.curveAmount = properties.curveAmount || this.defaults.curveAmount.value;
+        this.algorithmIndex = properties.algorithmIndex || this.defaults.algorithmIndex.value;
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Overdrive.prototype = Object.create(Super, {
+        name: {
+            value: "Overdrive"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                drive: {
+                    value: 1,
+                    min: 0,
+                    max: 1,
+                    automatable: true,
+                    type: FLOAT,
+                    scaled: true
+                },
+                outputGain: {
+                    value: 1,
+                    min: 0,
+                    max: 1,
+                    automatable: true,
+                    type: FLOAT,
+                    scaled: true
+                },
+                curveAmount: {
+                    value: 0.725,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                algorithmIndex: {
+                    value: 0,
+                    min: 0,
+                    max: 5,
+                    automatable: false,
+                    type: INT
+                }
+            }
+        },
+        k_nSamples: {
+            value: 8192
+        },
+        drive: {
+            get: function () {
+                return this.inputDrive.gain;
+            },
+            set: function (value) {
+                this._drive = value;
+            }
+        },
+        curveAmount: {
+            get: function () {
+                return this._curveAmount;
+            },
+            set: function (value) {
+                this._curveAmount = value;
+                if (this._algorithmIndex === undefined) {
+                    this._algorithmIndex = 0;
+                }
+                this.waveshaperAlgorithms[this._algorithmIndex](this._curveAmount, this.k_nSamples, this.ws_table);
+                this.waveshaper.curve = this.ws_table;
+            }
+        },
+        outputGain: {
+            get: function () {
+                return this.outputDrive.gain;
+            },
+            set: function (value) {
+                this._outputGain = dbToWAVolume(value);
+            }
+        },
+        algorithmIndex: {
+            get: function () {
+                return this._algorithmIndex;
+            },
+            set: function (value) {
+                this._algorithmIndex = value>>0;
+                this.curveAmount = this._curveAmount;
+            }
+        },
+        waveshaperAlgorithms: {
+            value: [
+
+            function (amount, n_samples, ws_table) {
+                amount = Math.min(amount, 0.9999);
+                var k = 2 * amount / (1 - amount),
+                    i, x;
+                for(i = 0; i < n_samples; i++) {
+                    x = i * 2 / n_samples - 1;
+                    ws_table[i] = (1 + k) * x / (1 + k * Math.abs(x));
+                }
+            }, function (amount, n_samples, ws_table) {
+                var i, x, y;
+                for(i = 0; i < n_samples; i++) {
+                    x = i * 2 / n_samples - 1;
+                    y = ((0.5 * Math.pow((x + 1.4), 2)) - 1) * y >= 0 ? 5.8 : 1.2;
+                    ws_table[i] = tanh(y);
+                }
+            }, function (amount, n_samples, ws_table) {
+                var i, x, y, a = 1 - amount;
+                for(i = 0; i < n_samples; i++) {
+                    x = i * 2 / n_samples - 1;
+                    y = x < 0 ? -Math.pow(Math.abs(x), a + 0.04) : Math.pow(x, a);
+                    ws_table[i] = tanh(y * 2);
+                }
+            }, function (amount, n_samples, ws_table) {
+                var i, x, y, abx, a = 1 - amount > 0.99 ? 0.99 : 1 - amount;
+                for(i = 0; i < n_samples; i++) {
+                    x = i * 2 / n_samples - 1;
+                    abx = Math.abs(x);
+                    if (abx < a) y = abx;
+                    else if (abx > a) y = a + (abx - a) / (1 + Math.pow((abx - a) / (1 - a), 2));
+                    else if (abx > 1) y = abx;
+                    ws_table[i] = sign(x) * y * (1 / ((a + 1) / 2));
+                }
+            }, function (amount, n_samples, ws_table) { // fixed curve, amount doesn't do anything, the distortion is just from the drive
+                var i, x;
+                for(i = 0; i < n_samples; i++) {
+                    x = i * 2 / n_samples - 1;
+                    if (x < -0.08905) {
+                        ws_table[i] = (-3 / 4) * (1 - (Math.pow((1 - (Math.abs(x) - 0.032857)), 12)) + (1 / 3) * (Math.abs(x) - 0.032847)) + 0.01;
+                    } else if (x >= -0.08905 && x < 0.320018) {
+                        ws_table[i] = (-6.153 * (x * x)) + 3.9375 * x;
+                    } else {
+                        ws_table[i] = 0.630035;
+                    }
+                }
+            }, function (amount, n_samples, ws_table) {
+                var a = 2 + Math.round(amount * 14),
+                    // we go from 2 to 16 bits, keep in mind for the UI
+                    bits = Math.round(Math.pow(2, a - 1)),
+                    // real number of quantization steps divided by 2
+                    i, x;
+                for(i = 0; i < n_samples; i++) {
+                    x = i * 2 / n_samples - 1;
+                    ws_table[i] = Math.round(x * bits) / bits;
+                }
+            }]
+        }
+    });
+    Tuna.prototype.Phaser = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.splitter = this.activateNode = userContext.createChannelSplitter(2);
+        this.filtersL = [];
+        this.filtersR = [];
+        this.feedbackGainNodeL = userContext.createGain();
+        this.feedbackGainNodeR = userContext.createGain();
+        this.merger = userContext.createChannelMerger(2);
+        this.filteredSignal = userContext.createGain();
+        this.output = userContext.createGain();
+        this.lfoL = new userInstance.LFO({
+            target: this.filtersL,
+            callback: this.callback
+        });
+        this.lfoR = new userInstance.LFO({
+            target: this.filtersR,
+            callback: this.callback
+        });
+
+        var i = this.stage;
+        while(i--) {
+            this.filtersL[i] = userContext.createBiquadFilter();
+            this.filtersR[i] = userContext.createBiquadFilter();
+            this.filtersL[i].type = this.filtersL[i].ALLPASS;
+            this.filtersR[i].type = this.filtersR[i].ALLPASS;
+        }
+        this.input.connect(this.splitter);
+        this.input.connect(this.output);
+        this.splitter.connect(this.filtersL[0], 0, 0);
+        this.splitter.connect(this.filtersR[0], 1, 0);
+        this.connectInOrder(this.filtersL);
+        this.connectInOrder(this.filtersR);
+        this.filtersL[this.stage - 1].connect(this.feedbackGainNodeL);
+        this.filtersL[this.stage - 1].connect(this.merger, 0, 0);
+        this.filtersR[this.stage - 1].connect(this.feedbackGainNodeR);
+        this.filtersR[this.stage - 1].connect(this.merger, 0, 1);
+        this.feedbackGainNodeL.connect(this.filtersL[0]);
+        this.feedbackGainNodeR.connect(this.filtersR[0]);
+        this.merger.connect(this.output);
+
+        this.rate = properties.rate || this.defaults.rate.value;
+        this.baseModulationFrequency = properties.baseModulationFrequency || this.defaults.baseModulationFrequency.value;
+        this.depth = properties.depth || this.defaults.depth.value;
+        this.feedback = properties.feedback || this.defaults.feedback.value;
+        this.stereoPhase = properties.stereoPhase || this.defaults.stereoPhase.value;
+
+        this.lfoL.activate(true);
+        this.lfoR.activate(true);
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Phaser.prototype = Object.create(Super, {
+        name: {
+            value: "Phaser"
+        },
+        stage: {
+            value: 4
+        },
+        defaults: {
+            writable:true,
+            value: {
+                rate: {
+                    value: 0.1,
+                    min: 0,
+                    max: 8,
+                    automatable: false,
+                },
+                depth: {
+                    value: 0.6,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                feedback: {
+                    value: 0.7,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                stereoPhase: {
+                    value: 40,
+                    min: 0,
+                    max: 180,
+                    automatable: false,
+                },
+                baseModulationFrequency: {
+                    value: 700,
+                    min: 500,
+                    max: 1500,
+                    automatable: false,
+                }
+            }
+        },
+        callback: {
+            value: function (filters, value) {
+                for(var stage = 0; stage < 4; stage++) {
+                    filters[stage].frequency.value = value;
+                }
+            }
+        },
+        depth: {
+            get: function () {
+                return this._depth;
+            },
+            set: function (value) {
+                this._depth = value;
+                this.lfoL.oscillation = this._baseModulationFrequency * this._depth;
+                this.lfoR.oscillation = this._baseModulationFrequency * this._depth;
+            }
+        },
+        rate: {
+            get: function () {
+                return this._rate;
+            },
+            set: function (value) {
+                this._rate = value;
+                this.lfoL.frequency = this._rate;
+                this.lfoR.frequency = this._rate;
+            }
+        },
+        baseModulationFrequency: {
+            enumerable: true,
+            get: function () {
+                return this._baseModulationFrequency;
+            },
+            set: function (value) {
+                this._baseModulationFrequency = value;
+                this.lfoL.offset = this._baseModulationFrequency;
+                this.lfoR.offset = this._baseModulationFrequency;
+                this._depth = this._depth;
+            }
+        },
+        feedback: {
+            get: function () {
+                return this._feedback;
+            },
+            set: function (value) {
+                this._feedback = value;
+                this.feedbackGainNodeL.gain.value = this._feedback;
+                this.feedbackGainNodeR.gain.value = this._feedback;
+            }
+        },
+        stereoPhase: {
+            get: function () {
+                return this._stereoPhase;
+            },
+            set: function (value) {
+                this._stereoPhase = value;
+                var newPhase = this.lfoL._phase + this._stereoPhase * Math.PI / 180;
+                newPhase = fmod(newPhase, 2 * Math.PI);
+                this.lfoR._phase = newPhase;
+            }
+        }
+    });
+    Tuna.prototype.Tremolo = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.splitter = this.activateNode = userContext.createChannelSplitter(2), this.amplitudeL = userContext.createGain(), this.amplitudeR = userContext.createGain(), this.merger = userContext.createChannelMerger(2), this.output = userContext.createGain();
+        this.lfoL = new userInstance.LFO({
+            target: this.amplitudeL.gain,
+            callback: pipe
+        });
+        this.lfoR = new userInstance.LFO({
+            target: this.amplitudeR.gain,
+            callback: pipe
+        });
+
+        this.input.connect(this.splitter);
+        this.splitter.connect(this.amplitudeL, 0);
+        this.splitter.connect(this.amplitudeR, 1);
+        this.amplitudeL.connect(this.merger, 0, 0);
+        this.amplitudeR.connect(this.merger, 0, 1);
+        this.merger.connect(this.output);
+
+        this.rate = properties.rate || this.defaults.rate.value;
+        this.intensity = properties.intensity || this.defaults.intensity.value;
+        this.stereoPhase = properties.stereoPhase || this.defaults.stereoPhase.value;
+
+        this.lfoL.offset = 1 - (this.intensity / 2);
+        this.lfoR.offset = 1 - (this.intensity / 2);
+        this.lfoL.phase = this.stereoPhase * Math.PI / 180;
+
+        this.lfoL.activate(true);
+        this.lfoR.activate(true);
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.Tremolo.prototype = Object.create(Super, {
+        name: {
+            value: "Tremolo"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                intensity: {
+                    value: 0.3,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                stereoPhase: {
+                    value: 0,
+                    min: 0,
+                    max: 180,
+                    automatable: false,
+                },
+                rate: {
+                    value: 5,
+                    min: 0.1,
+                    max: 11,
+                    automatable: false,
+                }
+            }
+        },
+        intensity: {
+            enumerable: true,
+            get: function () {
+                return this._intensity;
+            },
+            set: function (value) {
+                this._intensity = value;
+                this.lfoL.offset = 1 - this._intensity / 2;
+                this.lfoR.offset = 1 - this._intensity / 2;
+                this.lfoL.oscillation = this._intensity;
+                this.lfoR.oscillation = this._intensity;
+            }
+        },
+        rate: {
+            enumerable: true,
+            get: function () {
+                return this._rate;
+            },
+            set: function (value) {
+                this._rate = value;
+                this.lfoL.frequency = this._rate;
+                this.lfoR.frequency = this._rate;
+            }
+        },
+        stereoPhase: {
+            enumerable: true,
+            get: function () {
+                return this._rate;
+            },
+            set: function (value) {
+                this._stereoPhase = value;
+                var newPhase = this.lfoL._phase + this._stereoPhase * Math.PI / 180;
+                newPhase = fmod(newPhase, 2 * Math.PI);
+                this.lfoR.phase = newPhase;
+            }
+        }
+    });
+    Tuna.prototype.WahWah = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.envelopeFollower = new userInstance.EnvelopeFollower({
+            target: this,
+            callback: function (context, value) {
+                context.sweep = value;
+            }
+        });
+        this.filterBp = userContext.createBiquadFilter();
+        this.filterPeaking = userContext.createBiquadFilter();
+        this.output = userContext.createGain();
+
+        //Connect AudioNodes
+        this.activateNode.connect(this.filterBp);
+        this.filterBp.connect(this.filterPeaking);
+        this.filterPeaking.connect(this.output);
+
+        //Set Properties
+        this.init();
+        this.automode = properties.enableAutoMode || this.defaults.automode.value;
+        this.resonance = properties.resonance || this.defaults.resonance.value;
+        this.sensitivity = properties.sensitivity || this.defaults.sensitivity.value;
+        this.baseFrequency = properties.baseFrequency || this.defaults.baseFrequency.value;
+        this.excursionOctaves = properties.excursionOctaves || this.defaults.excursionOctaves.value;
+        this.sweep = properties.sweep || this.defaults.sweep.value;
+
+        this.activateNode.gain.value = 2;
+        this.envelopeFollower.activate(true);
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.WahWah.prototype = Object.create(Super, {
+        name: {
+            value: "WahWah"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                automode: {
+                    value: true,
+                    automatable: false,
+                    type: BOOLEAN
+                },
+                baseFrequency: {
+                    value: 0.5,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                excursionOctaves: {
+                    value: 2,
+                    min: 1,
+                    max: 6,
+                    automatable: false,
+                },
+                sweep: {
+                    value: 0.2,
+                    min: 0,
+                    max: 1,
+                    automatable: false,
+                },
+                resonance: {
+                    value: 10,
+                    min: 1,
+                    max: 100,
+                    automatable: false,
+                },
+                sensitivity: {
+                    value: 0.5,
+                    min: -1,
+                    max: 1,
+                    automatable: false,
+                }
+            }
+        },
+        activateCallback: {
+            value: function (value) {
+                this.automode = value;
+            }
+        },
+        automode: {
+            get: function () {
+                return this._automode;
+            },
+            set: function (value) {
+                this._automode = value;
+                if (value) {
+                    this.activateNode.connect(this.envelopeFollower.input);
+                    this.envelopeFollower.activate(true);
+                } else {
+                    this.envelopeFollower.activate(false);
+                    this.activateNode.disconnect();
+                    this.activateNode.connect(this.filterBp);
+                }
+            }
+        },
+        sweep: {
+            enumerable: true,
+            get: function () {
+                return this._sweep.value;
+            },
+            set: function (value) {
+                this._sweep = Math.pow(value > 1 ? 1 : value < 0 ? 0 : value, this._sensitivity);
+                this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep;
+                this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep;
+            }
+        },
+        baseFrequency: {
+            enumerable: true,
+            get: function () {
+                return this._baseFrequency;
+            },
+            set: function (value) {
+                this._baseFrequency = 50 * Math.pow(10, value * 2);
+                this._excursionFrequency = Math.min(this.sampleRate / 2, this.baseFrequency * Math.pow(2, this._excursionOctaves));
+                this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep;
+                this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep;
+            }
+        },
+        excursionOctaves: {
+            enumerable: true,
+            get: function () {
+                return this._excursionOctaves;
+            },
+            set: function (value) {
+                this._excursionOctaves = value;
+                this._excursionFrequency = Math.min(this.sampleRate / 2, this.baseFrequency * Math.pow(2, this._excursionOctaves));
+                this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep;
+                this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep;
+            }
+        },
+        sensitivity: {
+            enumerable: true,
+            get: function () {
+                return this._sensitivity;
+            },
+            set: function (value) {
+                this._sensitivity = Math.pow(10, value);
+            }
+        },
+        resonance: {
+            enumerable: true,
+            get: function () {
+                return this._resonance;
+            },
+            set: function (value) {
+                this._resonance = value;
+                this.filterPeaking.Q = this._resonance;
+            }
+        },
+        init: {
+            value: function () {
+                this.output.gain.value = 1;
+                this.filterPeaking.type = 5;
+                this.filterBp.type = 2;
+                this.filterPeaking.frequency.value = 100;
+                this.filterPeaking.gain.value = 20;
+                this.filterPeaking.Q.value = 5;
+                this.filterBp.frequency.value = 100;
+                this.filterBp.Q.value = 1;
+                this.sampleRate = userContext.sampleRate;
+            }
+        }
+    });
+    Tuna.prototype.EnvelopeFollower = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.jsNode = this.output = userContext.createScriptProcessor(this.buffersize, 1, 1);
+
+        this.input.connect(this.output);
+
+        this.attackTime = properties.attackTime || this.defaults.attackTime.value;
+        this.releaseTime = properties.releaseTime || this.defaults.releaseTime.value;
+        this._envelope = 0;
+        this.target = properties.target || {};
+        this.callback = properties.callback || function () {};
+    };
+    Tuna.prototype.EnvelopeFollower.prototype = Object.create(Super, {
+        name: {
+            value: "EnvelopeFollower"
+        },
+        defaults: {
+            value: {
+                attackTime: {
+                    value: 0.003,
+                    min: 0,
+                    max: 0.5,
+                    automatable: false,
+                },
+                releaseTime: {
+                    value: 0.5,
+                    min: 0,
+                    max: 0.5,
+                    automatable: false,
+                }
+            }
+        },
+        buffersize: {
+            value: 256
+        },
+        envelope: {
+            value: 0
+        },
+        sampleRate: {
+            value: 44100
+        },
+        attackTime: {
+            enumerable: true,
+            get: function () {
+                return this._attackTime;
+            },
+            set: function (value) {
+                this._attackTime = value;
+                this._attackC = Math.exp(-1 / this._attackTime * this.sampleRate / this.buffersize);
+            }
+        },
+        releaseTime: {
+            enumerable: true,
+            get: function () {
+                return this._releaseTime;
+            },
+            set: function (value) {
+                this._releaseTime = value;
+                this._releaseC = Math.exp(-1 / this._releaseTime * this.sampleRate / this.buffersize);
+            }
+        },
+        callback: {
+            get: function () {
+                return this._callback;
+            },
+            set: function (value) {
+                if (typeof value === "function") {
+                    this._callback = value;
+                } else {
+                    console.error("tuna.js: " + this.name + ": Callback must be a function!");
+                }
+            }
+        },
+        target: {
+            get: function () {
+                return this._target;
+            },
+            set: function (value) {
+                this._target = value;
+            }
+        },
+        activate: {
+            value: function (doActivate) {
+                this.activated = doActivate;
+                if (doActivate) {
+                    this.jsNode.connect(userContext.destination);
+                    this.jsNode.onaudioprocess = this.returnCompute(this);
+                } else {
+                    this.jsNode.disconnect();
+                    this.jsNode.onaudioprocess = null;
+                }
+            }
+        },
+        returnCompute: {
+            value: function (instance) {
+                return function (event) {
+                    instance.compute(event);
+                };
+            }
+        },
+        compute: {
+            value: function (event) {
+                var count = event.inputBuffer.getChannelData(0).length,
+                    channels = event.inputBuffer.numberOfChannels,
+                    current, chan, rms, i;
+                chan = rms = i = 0;
+                if (channels > 1) { //need to mixdown
+                    for(i = 0; i < count; ++i) {
+                        for(; chan < channels; ++chan) {
+                            current = event.inputBuffer.getChannelData(chan)[i];
+                            rms += (current * current) / channels;
+                        }
+                    }
+                } else {
+                    for(i = 0; i < count; ++i) {
+                        current = event.inputBuffer.getChannelData(0)[i];
+                        rms += (current * current);
+                    }
+                }
+                rms = Math.sqrt(rms);
+
+                if (this._envelope < rms) {
+                    this._envelope *= this._attackC;
+                    this._envelope += (1 - this._attackC) * rms;
+                } else {
+                    this._envelope *= this._releaseC;
+                    this._envelope += (1 - this._releaseC) * rms;
+                }
+                this._callback(this._target, this._envelope);
+            }
+        }
+    });
+    
+    // Low-frequency oscillation
+    Tuna.prototype.LFO = function (properties) {
+        //Instantiate AudioNode
+        this.output = userContext.createScriptProcessor(256, 1, 1);
+        this.activateNode = userContext.destination;
+
+        //Set Properties
+        this.frequency = properties.frequency || this.defaults.frequency.value;
+        this.offset = properties.offset || this.defaults.offset.value;
+        this.oscillation = properties.oscillation || this.defaults.oscillation.value;
+        this.phase = properties.phase || this.defaults.phase.value;
+        this.target = properties.target || {};
+        this.output.onaudioprocess = this.callback(properties.callback || function () {});
+        this.bypass = properties.bypass || false;
+    };
+    Tuna.prototype.LFO.prototype = Object.create(Super, {
+        name: {
+            value: "LFO"
+        },
+        bufferSize: {
+            value: 256
+        },
+        sampleRate: {
+            value: 44100
+        },
+        defaults: {
+            value: {
+                frequency: {
+                    value: 1,
+                    min: 0,
+                    max: 20,
+                    automatable: false,
+                },
+                offset: {
+                    value: 0.85,
+                    min: 0,
+                    max: 22049,
+                    automatable: false,
+                },
+                oscillation: {
+                    value: 0.3,
+                    min: -22050,
+                    max: 22050,
+                    automatable: false,
+                },
+                phase: {
+                    value: 0,
+                    min: 0,
+                    max: 2 * Math.PI,
+                    automatable: false,
+                }
+            }
+        },
+        frequency: {
+            get: function () {
+                return this._frequency;
+            },
+            set: function (value) {
+                this._frequency = value;
+                this._phaseInc = 2 * Math.PI * this._frequency * this.bufferSize / this.sampleRate;
+            }
+        },
+        offset: {
+            get: function () {
+                return this._offset;
+            },
+            set: function (value) {
+                this._offset = value;
+            }
+        },
+        oscillation: {
+            get: function () {
+                return this._oscillation;
+            },
+            set: function (value) {
+                this._oscillation = value;
+            }
+        },
+        phase: {
+            get: function () {
+                return this._phase;
+            },
+            set: function (value) {
+                this._phase = value;
+            }
+        },
+        target: {
+            get: function () {
+                return this._target;
+            },
+            set: function (value) {
+                this._target = value;
+            }
+        },
+        activate: {
+            value: function (doActivate) {
+                if (!doActivate) {
+                    this.output.disconnect(userContext.destination);
+                } else {
+                    this.output.connect(userContext.destination);
+                }
+            }
+        },
+        callback: {
+            value: function (callback) {
+                var that = this;
+                return function () {
+                    that._phase += that._phaseInc;
+                    if (that._phase > 2 * Math.PI) {
+                        that._phase = 0;
+                    }
+                    callback(that._target, that._offset + that._oscillation * Math.sin(that._phase));
+                };
+            }
+        }
+    });
+
+	/* Panner 
+	------------------------------------------------*/
+    Tuna.prototype.Panner = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.output = userContext.createGain();
+		this.panner = userContext.createPanner();
+
+        this.activateNode.connect(this.panner);
+		this.panner.connect(this.output);
+
+        this.bypass = properties.bypass || false;
+        this.x = properties.x || 0;
+        this.y = properties.y || 1;
+        this.z = properties.z || 1;
+        this.panningModel = properties.panningModel || 0;
+        this.distanceModel = properties.distanceModel || 0;
+    };
+
+	var PannerPosition = function(type) {
+		return {
+            enumerable: true,
+            get: function () {
+                return this["_" + type];
+            },
+            set: function (value) {
+                this["_" + type] = value;
+				this.panner.setPosition(this._x || 0, this._y || 0, this._z || 0);
+            }
+        }
+	};
+	
+	var PannerModel = function(type) {
+		return {
+            enumerable: true,
+            get: function () {
+                return this["_" + type];
+            },
+            set: function (value) {
+                this["_" + type] = value;
+				this.panner[type] = value;
+            }
+        }
+	};
+	
+	var Clamp = function(value, min, max, automatable) {
+		return {
+			value: value,
+			min: min,
+			max: max,
+			automatable: automatable
+		};
+	};
+	
+    Tuna.prototype.Panner.prototype = Object.create(Super, {
+        name: {
+            value: "Panner"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                x: Clamp(0, -20, 20, false),
+                y: Clamp(0, -20, 20, false),
+                z: Clamp(0, -20, 20, false),
+                distanceModel: Clamp(0, 0, 2, false),
+                panningModel: Clamp(0, 0, 2, false)
+            }
+        },
+        x: PannerPosition("x"),
+        y: PannerPosition("y"),
+        z: PannerPosition("z"),
+        panningModel: PannerModel("panningModel"),
+        distanceModel: PannerModel("distanceModel")
+    });
+
+	/* Volume 
+	------------------------------------------------*/
+    Tuna.prototype.Volume = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.output = userContext.createGain();
+
+        this.activateNode.connect(this.output);
+
+        this.bypass = properties.bypass || false;
+        this.amount = properties.amount || this.defaults.amount.value;
+    };
+
+    Tuna.prototype.Volume.prototype = Object.create(Super, {
+        name: {
+            value: "Volume"
+        },
+        defaults: {
+            writable:true,
+            value: {
+                amount: Clamp(0, 0, 2, false)
+            }
+        },
+        amount: {
+            enumerable: true,
+            get: function () {
+                return this._volume;
+            },
+            set: function (value) {
+                this._volume = value;
+                this.activateNode.gain.value = value;
+            }
+        }
+    });
+
+	/* Frequency 
+	------------------------------------------------*/
+    Tuna.prototype.Frequency = function (properties) {
+        if (!properties) {
+            properties = this.getDefaults();
+        }
+
+		this.trebleFilter = userContext.createBiquadFilter();
+		this.trebleFilter.type = this.trebleFilter.HIGHSHELF;
+		this.trebleFilter.frequency.value = 8000; // 1k+
+		this.trebleFilter.Q.value = 0;
+		this.midtoneFilter = userContext.createBiquadFilter();
+		this.midtoneFilter.type = this.midtoneFilter.PEAKING;
+		this.midtoneFilter.frequency.value = 1000; // 200-1k
+		this.midtoneFilter.Q.value = 0;
+		this.bassFilter = userContext.createBiquadFilter();
+		this.bassFilter.type = this.bassFilter.LOWSHELF;
+		this.bassFilter.frequency.value = 200; // 60-200k
+		this.bassFilter.Q.value = 0;
+		
+        this.input = userContext.createGain();
+        this.activateNode = userContext.createGain();
+        this.output = userContext.createGain();
+
+		this.activateNode.connect(this.bassFilter);
+		this.bassFilter.connect(this.midtoneFilter);
+		this.midtoneFilter.connect(this.trebleFilter);
+        this.trebleFilter.connect(this.output);
+
+        this.bypass = properties.bypass || false;
+        this.volume = properties.volume || false;
+        this.treble = properties.treble || false;
+        this.midtone = properties.midtone || false;
+        this.bass = properties.bass || false;
+    };
+
+    var GainValue = function(type, nodeId) {
+    	return {
+            enumerable: true,
+            get: function () {
+                return this["_" + type];
+            },
+            set: function (value) {
+                this["_" + type] = value;
+                this[nodeId || type + "Filter"].gain.value = value;
+            }
+        };
+    };
+
+    Tuna.prototype.Frequency.prototype = Object.create(Super, {
+        name: {
+            value: "Frequency"
+        },
+        defaults: {
+            writable:true,
+            value: {
+            	volume: Clamp(1, 0, 2, false),
+                treble: Clamp(0, -20, 20, false),
+                midtone: Clamp(0, -20, 20, false),
+                bass: Clamp(0, -20, 20, false)
+            }
+        },
+        volume: GainValue("volume", "activateNode"),
+        treble: GainValue("treble"),
+        midtone: GainValue("midtone"),
+        bass: GainValue("bass")
+    });
+
+    if (typeof define === "function") {
+        define("Tuna", [], function () {
+            return Tuna;
+        });
+    } else {
+        window.Tuna = Tuna;
+    }
+})(this);

+ 260 - 0
index.html

@@ -0,0 +1,260 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+    <script src="./inc/shim/Base64.js"></script>
+    <script src="./inc/shim/Base64binary.js"></script>
+    <script src="./inc/shim/WebAudioAPI.js"></script>
+    <script src="./inc/shim/WebMIDIAPI.js" type="text/javascript"></script>
+
+    <!-- jasmid package -->
+    <script src="./inc/jasmid/stream.js"></script>
+    <script src="./inc/jasmid/midifile.js"></script>
+    <script src="./inc/jasmid/replayer.js"></script>
+
+    <script src="./js/midi/audioDetect.js" type="text/javascript"></script>
+    <script src="./js/midi/gm.js"></script>
+    <script src="./js/midi/loader.js"></script>
+    <script src="./js/midi/plugin.audiotag.js"></script>
+    <script src="./js/midi/plugin.webaudio.js"></script>
+    <script src="./js/midi/plugin.webmidi.js"></script>
+    <script src="./js/midi/player.js" type="text/javascript"></script>
+    <script src="./js/midi/audioDetect.js"></script>
+    <script src="./js/util/dom_request_xhr.js" type="text/javascript"></script>
+    <script src="./js/util/dom_request_script.js" type="text/javascript"></script>
+    <style>
+        body {
+            margin: 0px;
+            padding: 0px;
+        }
+
+        select,
+        button {
+            border: 1px solid black;
+            background-color: transparent;
+            outline: none;
+            font-weight: 400;
+            padding: 1px 6px;
+        }
+
+        .huaji {
+            background-color: transparent;
+            background-image: url('./images/huaji1.png');
+            background-repeat: no-repeat;
+            background-size: contain;
+            width: 28px;
+            padding: 0px;
+            padding-top: 59px;
+            border: 0px;
+            outline: 0px;
+            appearance: none;
+        }
+
+        .huaji.down {
+            background-image: url('./images/huaji2.png');
+        }
+
+        #openFile {
+            display: none;
+        }
+
+        .play-mode {
+            overflow: auto;
+            word-wrap: none;
+            white-space: nowrap;
+            position: fixed;
+            bottom: 0px;
+            left: 0px;
+            right: 0px;
+        }
+
+        .play-mode .huaji {
+            width: 56px;
+            padding-top: 118px;
+        }
+
+        #time {
+            width: 40%;
+            pointer-events: none;
+        }
+
+        .options>* {
+            vertical-align: middle;
+        }
+
+        .options {
+            padding: 10px 0px;
+            overflow: auto;
+            word-wrap: none;
+            white-space: nowrap;
+        }
+
+        .options>*:first-child {
+            margin-left: 8px !important;
+        }
+
+        .options>*:last-child {
+            margin-right: 8px !important;
+        }
+
+        #keyboard {
+            margin: auto 8px;
+        }
+    </style>
+</head>
+
+<body>
+    <div class="options">
+        <select onchange="openUrl('./midis/'+this.value+'.mid')">
+            <option value="1" disabled selected>选择预置的乐谱</option>
+            <optgroup label="万恶之源">
+                <option value="Astronomia">Astronomia</option>
+                <option value="aLIEz">aLIEz</option>
+            </optgroup>
+            <optgroup label="火星文">
+                <option value="aLIEz">aLIEz</option>
+            </optgroup>
+            <optgroup label="某科学的超电磁炮">
+                <option value="Only my railgun (full ver)">Only my railgun</option>
+                <option value="Level 5 - Judgelight">Level 5 - Judgelight</option>
+                <option value="Sister's Noise">Sister's Noise</option>
+            </optgroup>
+            <optgroup label="LoveLive!">
+                <option value="SnowHalation">Snow Halation</option>
+                <option value="Arifutarekanashiminohate">ありふれた悲しみの果て</option>
+            </optgroup>
+        </select>
+        <input type="file" id="openFile" accept="audio/midi" onchange="openFile()">
+        <button onclick="document.getElementById('openFile').click()">打开</button>
+        <button id="play" onclick="play()">播放</button>
+        <input type="range" id="time" value="0" readonly>
+        <button onclick="stop()">停止</button>
+        <label><input type="checkbox" onchange="modeChange()">弹奏模式</label>
+    </div>
+    <div style="margin: 0px 8px;"><i>如果你发现音乐好像有哪里不对,点一下停止然后重新开始播放就好!</i></div>
+    <div id="keyboard">
+    </div>
+    <!-- <script src="midi.min.js"></script> -->
+    <script>
+        console.log(MIDI)
+        var player;
+        window.onload = function () {
+            var press = false;
+            var setPressTimeout;
+            var keyboard = document.getElementById("keyboard")
+            var buttons = []
+            var time = document.getElementById("time")
+            // console.log({keyboard})
+            for (let index = 0; index < 88; index++) {
+                var button = document.createElement("BUTTON")
+                button.className = 'huaji';
+                // button.classList.add("huaji")
+                button.onmousedown = function () {
+                    this.classList.add("down")
+                    keyDown(index + 21)
+                    press = true
+                }
+                button.onmouseenter = function () {
+                    if (press) {
+                        this.classList.add("down")
+                        keyDown(index + 21)
+                        if (setPressTimeout) clearTimeout(setPressTimeout)
+                    }
+                    // console.log(index + 21 + "enter")
+                }
+                button.onmouseup = function () {
+                    this.classList.remove("down")
+                    keyUp(index + 21)
+                    press = false
+                }
+                button.onmouseleave = function () {
+                    this.classList.remove("down")
+                    keyUp(index + 21)
+                    if (press) setPressTimeout = setTimeout(function () {
+                        press = false
+                    }, 500);
+                }
+                button.innerText = MIDI.noteToKey[index + 21]
+                buttons.push(button)
+                keyboard.appendChild(button)
+            }
+            MIDI.loadPlugin({
+                soundfontUrl: "./soundfont/",
+                instruments: "acoustic_grand_piano",
+                onprogress: function (state, progress) {
+                    // console.log(state, progress);
+                },
+                onsuccess: function () {
+                    var delay = 0; // play one note every quarter second
+                    var note = 50; // the MIDI note
+                    var velocity = 127; // how hard the note hits
+                    player = MIDI.Player
+                    player.addListener(function (data) {
+                        // console.log(data)
+                        var pianoKey = data.note - 21;
+                        // console.log('note', MIDI.noteToKey[data.note])
+                        if (data.message == 144) {
+                            time.value = data.now
+                            time.max = data.end
+                            buttons[pianoKey] && buttons[pianoKey].classList.add("down")
+                        } else {
+                            buttons[pianoKey] && buttons[pianoKey].classList.remove("down")
+                        }
+                    });
+                    player.timeWarp = 1; // speed the song is played back
+                    // play the note
+                    MIDI.setVolume(0, 127);
+                }
+            });
+            function keyDown(note) {
+                MIDI.noteOn(0, note, 127, 0);
+            }
+            function keyUp(note) {
+                MIDI.noteOff(0, note, 0);
+            }
+        };
+        function openFile() {
+            // console.log("openFile",document.getElementById('openFile').files)
+            if (document.getElementById('openFile').files.length > 0 && player) {
+                playUrl(URL.createObjectURL(document.getElementById('openFile').files[0]))
+            }
+        }
+        function openUrl(url) {
+            player && playUrl(url)
+        }
+        function playUrl(url) {
+            // console.log(player)
+            player.loadFile(url, function () {
+                player.start()
+                document.getElementById("play").innerText = "暂停"
+            });
+
+        }
+        function modeChange() {
+            var keyboard = document.getElementById("keyboard")
+            keyboard.classList[keyboard.classList.contains("play-mode") ? "remove" : "add"]("play-mode")
+        }
+        function play() {
+            if (player && player.currentTime !== player.endTime) {
+                if (player.playing) {
+                    document.getElementById("play").innerText = "播放"
+                    player.pause(true);
+                } else {
+                    document.getElementById("play").innerText = "暂停"
+                    player.resume();
+                }
+            }
+        }
+        function stop() {
+            if (player) {
+                document.getElementById("play").innerText = "播放"
+                player.stop();
+            }
+        }
+    </script>
+</body>
+
+</html>

File diff suppressed because it is too large
+ 85 - 0
js/midi/audioDetect.js


+ 161 - 0
js/midi/gm.js

@@ -0,0 +1,161 @@
+/*
+	----------------------------------------------------------
+	GeneralMIDI
+	----------------------------------------------------------
+*/
+
+(function(root) { 'use strict';
+
+	root.GM = (function(arr) {
+		var clean = function(name) {
+			return name.replace(/[^a-z0-9 ]/gi, '').replace(/[ ]/g, '_').toLowerCase();
+		};
+		var res = {
+			byName: { },
+			byId: { },
+			byCategory: { }
+		};
+		for (var key in arr) {
+			var list = arr[key];
+			for (var n = 0, length = list.length; n < length; n++) {
+				var instrument = list[n];
+				if (!instrument) continue;
+				var num = parseInt(instrument.substr(0, instrument.indexOf(' ')), 10);
+				instrument = instrument.replace(num + ' ', '');
+				res.byId[--num] = 
+				res.byName[clean(instrument)] = 
+				res.byCategory[clean(key)] = {
+					id: clean(instrument),
+					instrument: instrument,
+					number: num,
+					category: key
+				};
+			}
+		}
+		return res;
+	})({
+		'Piano': ['1 Acoustic Grand Piano', '2 Bright Acoustic Piano', '3 Electric Grand Piano', '4 Honky-tonk Piano', '5 Electric Piano 1', '6 Electric Piano 2', '7 Harpsichord', '8 Clavinet'],
+		'Chromatic Percussion': ['9 Celesta', '10 Glockenspiel', '11 Music Box', '12 Vibraphone', '13 Marimba', '14 Xylophone', '15 Tubular Bells', '16 Dulcimer'],
+		'Organ': ['17 Drawbar Organ', '18 Percussive Organ', '19 Rock Organ', '20 Church Organ', '21 Reed Organ', '22 Accordion', '23 Harmonica', '24 Tango Accordion'],
+		'Guitar': ['25 Acoustic Guitar (nylon)', '26 Acoustic Guitar (steel)', '27 Electric Guitar (jazz)', '28 Electric Guitar (clean)', '29 Electric Guitar (muted)', '30 Overdriven Guitar', '31 Distortion Guitar', '32 Guitar Harmonics'],
+		'Bass': ['33 Acoustic Bass', '34 Electric Bass (finger)', '35 Electric Bass (pick)', '36 Fretless Bass', '37 Slap Bass 1', '38 Slap Bass 2', '39 Synth Bass 1', '40 Synth Bass 2'],
+		'Strings': ['41 Violin', '42 Viola', '43 Cello', '44 Contrabass', '45 Tremolo Strings', '46 Pizzicato Strings', '47 Orchestral Harp', '48 Timpani'],
+		'Ensemble': ['49 String Ensemble 1', '50 String Ensemble 2', '51 Synth Strings 1', '52 Synth Strings 2', '53 Choir Aahs', '54 Voice Oohs', '55 Synth Choir', '56 Orchestra Hit'],
+		'Brass': ['57 Trumpet', '58 Trombone', '59 Tuba', '60 Muted Trumpet', '61 French Horn', '62 Brass Section', '63 Synth Brass 1', '64 Synth Brass 2'],
+		'Reed': ['65 Soprano Sax', '66 Alto Sax', '67 Tenor Sax', '68 Baritone Sax', '69 Oboe', '70 English Horn', '71 Bassoon', '72 Clarinet'],
+		'Pipe': ['73 Piccolo', '74 Flute', '75 Recorder', '76 Pan Flute', '77 Blown Bottle', '78 Shakuhachi', '79 Whistle', '80 Ocarina'],
+		'Synth Lead': ['81 Lead 1 (square)', '82 Lead 2 (sawtooth)', '83 Lead 3 (calliope)', '84 Lead 4 (chiff)', '85 Lead 5 (charang)', '86 Lead 6 (voice)', '87 Lead 7 (fifths)', '88 Lead 8 (bass + lead)'],
+		'Synth Pad': ['89 Pad 1 (new age)', '90 Pad 2 (warm)', '91 Pad 3 (polysynth)', '92 Pad 4 (choir)', '93 Pad 5 (bowed)', '94 Pad 6 (metallic)', '95 Pad 7 (halo)', '96 Pad 8 (sweep)'],
+		'Synth Effects': ['97 FX 1 (rain)', '98 FX 2 (soundtrack)', '99 FX 3 (crystal)', '100 FX 4 (atmosphere)', '101 FX 5 (brightness)', '102 FX 6 (goblins)', '103 FX 7 (echoes)', '104 FX 8 (sci-fi)'],
+		'Ethnic': ['105 Sitar', '106 Banjo', '107 Shamisen', '108 Koto', '109 Kalimba', '110 Bagpipe', '111 Fiddle', '112 Shanai'],
+		'Percussive': ['113 Tinkle Bell', '114 Agogo', '115 Steel Drums', '116 Woodblock', '117 Taiko Drum', '118 Melodic Tom', '119 Synth Drum'],
+		'Sound effects': ['120 Reverse Cymbal', '121 Guitar Fret Noise', '122 Breath Noise', '123 Seashore', '124 Bird Tweet', '125 Telephone Ring', '126 Helicopter', '127 Applause', '128 Gunshot']
+	});
+
+	/* get/setInstrument
+	--------------------------------------------------- */
+	root.getInstrument = function(channelId) {
+		var channel = root.channels[channelId];
+		return channel && channel.instrument;
+	};
+
+	root.setInstrument = function(channelId, program, delay) {
+		var channel = root.channels[channelId];
+		if (delay) {
+			return setTimeout(function() {
+				channel.instrument = program;
+			}, delay);
+		} else {
+			channel.instrument = program;
+		}
+	};
+
+	/* get/setMono
+	--------------------------------------------------- */
+	root.getMono = function(channelId) {
+		var channel = root.channels[channelId];
+		return channel && channel.mono;
+	};
+
+	root.setMono = function(channelId, truthy, delay) {
+		var channel = root.channels[channelId];
+		if (delay) {
+			return setTimeout(function() {
+				channel.mono = truthy;
+			}, delay);
+		} else {
+			channel.mono = truthy;
+		}
+	};
+
+	/* get/setOmni
+	--------------------------------------------------- */
+	root.getOmni = function(channelId) {
+		var channel = root.channels[channelId];
+		return channel && channel.omni;
+	};
+
+	root.setOmni = function(channelId, truthy) {
+		var channel = root.channels[channelId];
+		if (delay) {
+			return setTimeout(function() {
+				channel.omni = truthy;	
+			}, delay);
+		} else {
+			channel.omni = truthy;
+		}
+	};
+
+	/* get/setSolo
+	--------------------------------------------------- */
+	root.getSolo = function(channelId) {
+		var channel = root.channels[channelId];
+		return channel && channel.solo;
+	};
+
+	root.setSolo = function(channelId, truthy) {
+		var channel = root.channels[channelId];
+		if (delay) {
+			return setTimeout(function() {
+				channel.solo = truthy;	
+			}, delay);
+		} else {
+			channel.solo = truthy;
+		}
+	};
+
+	/* channels
+	--------------------------------------------------- */
+	root.channels = (function() { // 0 - 15 channels
+		var channels = {};
+		for (var i = 0; i < 16; i++) {
+			channels[i] = { // default values
+				instrument: i,
+				pitchBend: 0,
+				mute: false,
+				mono: false,
+				omni: false,
+				solo: false
+			};
+		}
+		return channels;
+	})();
+
+	/* note conversions
+	--------------------------------------------------- */
+	root.keyToNote = {}; // C8  == 108
+	root.noteToKey = {}; // 108 ==  C8
+
+	(function() {
+		var A0 = 0x15; // first note
+		var C8 = 0x6C; // last note
+		var number2key = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
+		for (var n = A0; n <= C8; n++) {
+			var octave = (n - 12) / 12 >> 0;
+			var name = number2key[n % 12] + octave;
+			root.keyToNote[name] = n;
+			root.noteToKey[n] = name;
+		}
+	})();
+
+})(MIDI);

+ 199 - 0
js/midi/loader.js

@@ -0,0 +1,199 @@
+/*
+	----------------------------------------------------------
+	MIDI.Plugin : 0.3.4 : 2015-03-26
+	----------------------------------------------------------
+	https://github.com/mudcube/MIDI.js
+	----------------------------------------------------------
+	Inspired by javax.sound.midi (albeit a super simple version): 
+		http://docs.oracle.com/javase/6/docs/api/javax/sound/midi/package-summary.html
+	----------------------------------------------------------
+	Technologies
+	----------------------------------------------------------
+		Web MIDI API - no native support yet (jazzplugin)
+		Web Audio API - firefox 25+, chrome 10+, safari 6+, opera 15+
+		HTML5 Audio Tag - ie 9+, firefox 3.5+, chrome 4+, safari 4+, opera 9.5+, ios 4+, android 2.3+
+	----------------------------------------------------------
+*/
+
+if (typeof MIDI === 'undefined') MIDI = {};
+
+MIDI.Soundfont = MIDI.Soundfont || {};
+MIDI.Player = MIDI.Player || {};
+
+(function(root) { 'use strict';
+
+	root.DEBUG = true;
+	root.USE_XHR = true;
+	root.soundfontUrl = './soundfont/';
+
+	/*
+		MIDI.loadPlugin({
+			onsuccess: function() { },
+			onprogress: function(state, percent) { },
+			targetFormat: 'mp3', // optionally can force to use MP3 (for instance on mobile networks)
+			instrument: 'acoustic_grand_piano', // or 1 (default)
+			instruments: [ 'acoustic_grand_piano', 'acoustic_guitar_nylon' ] // or multiple instruments
+		});
+	*/
+
+	root.loadPlugin = function(opts) {
+		if (typeof opts === 'function') {
+			opts = {onsuccess: opts};
+		}
+
+		root.soundfontUrl = opts.soundfontUrl || root.soundfontUrl;
+
+		/// Detect the best type of audio to use
+		root.audioDetect(function(supports) {
+			var hash = window.location.hash;
+			var api = '';
+
+			/// use the most appropriate plugin if not specified
+			if (supports[opts.api]) {
+				api = opts.api;
+			} else if (supports[hash.substr(1)]) {
+				api = hash.substr(1);
+			} else if (supports.webmidi) {
+				api = 'webmidi';
+			} else if (window.AudioContext) { // Chrome
+				api = 'webaudio';
+			} else if (window.Audio) { // Firefox
+				api = 'audiotag';
+			}
+
+			if (connect[api]) {
+				/// use audio/ogg when supported
+				if (opts.targetFormat) {
+					var audioFormat = opts.targetFormat;
+				} else { // use best quality
+					var audioFormat = supports['audio/ogg'] ? 'ogg' : 'mp3';
+				}
+
+				/// load the specified plugin
+				root.__api = api;
+				root.__audioFormat = audioFormat;
+				root.supports = supports;
+				root.loadResource(opts);
+			}
+		});
+	};
+
+	/*
+		root.loadResource({
+			onsuccess: function() { },
+			onprogress: function(state, percent) { },
+			instrument: 'banjo'
+		})
+	*/
+
+	root.loadResource = function(opts) {
+		var instruments = opts.instruments || opts.instrument || 'acoustic_grand_piano';
+		///
+		if (typeof instruments !== 'object') {
+			if (instruments || instruments === 0) {
+				instruments = [instruments];
+			} else {
+				instruments = [];
+			}
+		}
+		/// convert numeric ids into strings
+		for (var i = 0; i < instruments.length; i ++) {
+			var instrument = instruments[i];
+			if (instrument === +instrument) { // is numeric
+				if (root.GM.byId[instrument]) {
+					instruments[i] = root.GM.byId[instrument].id;
+				}
+			}
+		}
+		///
+		opts.format = root.__audioFormat;
+		opts.instruments = instruments;
+		///
+		connect[root.__api](opts);
+	};
+
+	var connect = {
+		webmidi: function(opts) {
+			// cant wait for this to be standardized!
+			root.WebMIDI.connect(opts);
+		},
+		audiotag: function(opts) {
+			// works ok, kinda like a drunken tuna fish, across the board
+			// http://caniuse.com/audio
+			requestQueue(opts, 'AudioTag');
+		},
+		webaudio: function(opts) {
+			// works awesome! safari, chrome and firefox support
+			// http://caniuse.com/web-audio
+			requestQueue(opts, 'WebAudio');
+		}
+	};
+
+	var requestQueue = function(opts, context) {
+		var audioFormat = opts.format;
+		var instruments = opts.instruments;
+		var onprogress = opts.onprogress;
+		var onerror = opts.onerror;
+		///
+		var length = instruments.length;
+		var pending = length;
+		var waitForEnd = function() {
+			if (!--pending) {
+				onprogress && onprogress('load', 1.0);
+				root[context].connect(opts);
+			}
+		};
+		///
+		for (var i = 0; i < length; i ++) {
+			var instrumentId = instruments[i];
+			if (MIDI.Soundfont[instrumentId]) { // already loaded
+				waitForEnd();
+			} else { // needs to be requested
+				sendRequest(instruments[i], audioFormat, function(evt, progress) {
+					var fileProgress = progress / length;
+					var queueProgress = (length - pending) / length;
+					onprogress && onprogress('load', fileProgress + queueProgress, instrumentId);
+				}, function() {
+					waitForEnd();
+				}, onerror);
+			}
+		};
+	};
+
+	var sendRequest = function(instrumentId, audioFormat, onprogress, onsuccess, onerror) {
+		var soundfontPath = root.soundfontUrl + instrumentId + '-' + audioFormat + '.js';
+		if (root.USE_XHR) {
+			root.util.request({
+				url: soundfontPath,
+				format: 'text',
+				onerror: onerror,
+				onprogress: onprogress,
+				onsuccess: function(event, responseText) {
+					var script = document.createElement('script');
+					script.language = 'javascript';
+					script.type = 'text/javascript';
+					script.text = responseText;
+					document.body.appendChild(script);
+					///
+					onsuccess();
+				}
+			});
+		} else {
+			dom.loadScript.add({
+				url: soundfontPath,
+				verify: 'MIDI.Soundfont["' + instrumentId + '"]',
+				onerror: onerror,
+				onsuccess: function() {
+					onsuccess();
+				}
+			});
+		}
+	};
+
+	root.setDefaultPlugin = function(midi) {
+		for (var key in midi) {
+			root[key] = midi[key];
+		}
+	};
+
+})(MIDI);

+ 192 - 0
js/midi/player.js

@@ -0,0 +1,192 @@
+"undefined" === typeof MIDI && (MIDI = {});
+"undefined" === typeof MIDI.Player && (MIDI.Player = {});
+(function() {
+    var b = MIDI.Player;
+    b.callback = void 0;
+    b.currentTime = 0;
+    b.endTime = 0;
+    b.restart = 0;
+    b.playing = !1;
+    b.timeWarp = 1;
+    b.start = b.resume = function() {
+      -1 > b.currentTime && (b.currentTime = -1);
+      l(b.currentTime)
+    }
+    ;
+    b.pause = function() {
+      var a = b.restart;
+      n();
+      b.restart = a
+    }
+    ;
+    b.stop = function() {
+      n();
+      b.restart = 0;
+      b.currentTime = 0
+    }
+    ;
+    b.addListener = function(a) {
+      k = a
+    }
+    ;
+    b.removeListener = function() {
+      k = void 0
+    }
+    ;
+    b.clearAnimation = function() {
+      b.interval && window.clearInterval(b.interval)
+    }
+    ;
+    b.setAnimation = function(a) {
+      var c = "function" === typeof a ? a : a.callback;
+      a = a.interval || 30;
+      var e = 0
+        , k = 0
+        , f = 0;
+      b.clearAnimation();
+      b.interval = window.setInterval(function() {
+        if (0 !== b.endTime) {
+          b.playing ? (e = f === b.currentTime ? k - (new Date).getTime() : 0,
+            e = 0 === b.currentTime ? 0 : b.currentTime - e,
+          f !== b.currentTime && (k = (new Date).getTime(),
+            f = b.currentTime)) : e = b.currentTime;
+          var a = e / 1E3
+            , l = a / 60
+            , a = 60 * l + (a - 60 * l)
+            , l = b.endTime / 1E3;
+          -1 > l - a || c({
+            now: a,
+            end: l,
+            events: d
+          })
+        }
+      }, a)
+    }
+    ;
+    b.loadMidiFile = function() {
+      b.replayer = new Replayer(MidiFile(b.currentData),b.timeWarp);
+      b.data = b.replayer.getData();
+      b.endTime = f()
+    }
+    ;
+    b.loadFile = function(a, d) {
+      b.stop();
+      if (-1 !== a.indexOf("base64,")) {
+        var c = window.atob(a.split(",")[1]);
+        b.currentData = c;
+        b.loadMidiFile();
+        d && d(c)
+      } else
+        c = new XMLHttpRequest,
+          c.open("GET", a),
+          c.overrideMimeType("text/plain; charset=x-user-defined"),
+          c.onreadystatechange = function() {
+            if (4 === this.readyState && 200 === this.status) {
+              for (var a = this.responseText || "", c = [], e = a.length, k = String.fromCharCode, f = 0; f < e; f++)
+                c[f] = k(a.charCodeAt(f) & 255);
+              a = c.join("");
+              b.currentData = a;
+              b.loadMidiFile();
+              d && d(a)
+            }
+          }
+          ,
+          c.send()
+    }
+    ;
+    var a = [], c, e = 0, d = {}, k = void 0, m = function(a, e, f, n, g, m) {
+      return window.setTimeout(function() {
+        var n = {
+          channel: a,
+          note: e,
+          now: f,
+          end: b.endTime,
+          message: g,
+          velocity: m
+        };
+        128 === g ? delete d[e] : d[e] = n;
+        k && k(n);
+        b.currentTime = f;
+        b.currentTime === c && c < b.endTime && l(c, !0)
+      }, f - n)
+    }, g = function() {
+      if ("WebAudioAPI" === MIDI.lang)
+        return MIDI.Player.ctx;
+      b.ctx || (b.ctx = {
+        currentTime: 0
+      });
+      return b.ctx
+    }, f = function() {
+      for (var a = b.data, d = a.length, c = 0.5, e = 0; e < d; e++)
+        c += a[e][1];
+      return c
+    }, l = function(d, k) {
+      if (b.replayer) {
+        k || ("undefined" === typeof d && (d = b.restart),
+        b.playing && n(),
+          b.playing = !0,
+          b.data = b.replayer.getData(),
+          b.endTime = f());
+        var l, s = 0, t = 0, v = b.data, w = g(), F = v.length;
+        c = 0.5;
+        e = w.currentTime;
+        for (var B = 0; B < F; B++)
+          if (l = v[B],
+            l = l[1] || 1E-11,
+          c + l <= d)
+            s = c += l;
+          else
+            break;
+        for (; B < F && 100 > t; B++) {
+          l = v[B];
+          c += l[1] || 1E-11;
+          d = c - s;
+          var x = l[0].event;
+          if ("channel" === x.type) {
+            var u = x.channel;
+            switch (x.subtype) {
+              case "noteOn":
+                if (MIDI.channels[u].mute)
+                  break;
+                l = x.noteNumber - (b.MIDIOffset || 0);
+                a.push({
+                  event: x,
+                  source: MIDI.noteOn(u, x.noteNumber, x.velocity, d / 1E3 + w.currentTime),
+                  interval: m(u, l, c, s, 144, x.velocity)
+                });
+                t++;
+                break;
+              case "noteOff":
+                if (MIDI.channels[u].mute)
+                  break;
+                l = x.noteNumber - (b.MIDIOffset || 0);
+                a.push({
+                  event: x,
+                  source: MIDI.noteOff(u, x.noteNumber, d / 1E3 + w.currentTime),
+                  interval: m(u, l, c, s, 128)
+                })
+            }
+          }
+        }
+      }
+    }, n = function() {
+      var c = g();
+      b.playing = !1;
+      for (b.restart += 1E3 * (c.currentTime - e); a.length; )
+        c = a.pop(),
+          window.clearInterval(c.interval),
+        c.source && ("number" === typeof c.source ? window.clearTimeout(c.source) : c.source.disconnect(0));
+      for (var f in d)
+        c = d[f],
+        144 === d[f].message && k && k({
+          channel: c.channel,
+          note: c.note,
+          now: c.now,
+          end: c.end,
+          message: 128,
+          velocity: c.velocity
+        });
+      d = {}
+    }
+  }
+)();

+ 387 - 0
js/midi/player2.js

@@ -0,0 +1,387 @@
+/*
+	----------------------------------------------------------
+	MIDI.Player : 0.3.2 : 2017-12-31
+	----------------------------------------------------------
+	https://github.com/mudcube/MIDI.js
+	----------------------------------------------------------
+*/
+
+if (typeof MIDI === 'undefined') MIDI = {};
+if (typeof MIDI.Player === 'undefined') MIDI.Player = {};
+
+(function() { 'use strict';
+
+  var midi = MIDI.Player;
+  midi.currentTime = 0;
+  midi.endTime = 0;
+  midi.restart = 0;
+  midi.playing = false;
+  midi.timeWarp = 1;
+  midi.startDelay = 0;
+  midi.BPM = 120;
+  midi.playingStartTime = 0;
+  midi.ctxStartTime = 0;
+  midi.lastCallbackTime = 0;
+
+  midi.start =
+    midi.resume = function(onsuccess) {
+      if (midi.currentTime < -1) {
+        midi.currentTime = -1;
+      }
+      startAudio(midi.currentTime, null, onsuccess);
+    };
+
+  midi.pause = function() {
+    var tmp = midi.restart;
+    stopAudio();
+    midi.restart = tmp;
+  };
+
+  midi.stop = function() {
+    stopAudio();
+    midi.restart = 0;
+    midi.currentTime = 0;
+  };
+
+  midi.addListener = function(onsuccess) {
+    onMidiEvent = onsuccess;
+  };
+
+  midi.removeListener = function() {
+    onMidiEvent = undefined;
+  };
+
+  midi.clearAnimation = function() {
+    if (midi.animationFrameId)  {
+      cancelAnimationFrame(midi.animationFrameId);
+    }
+  };
+
+  midi.setAnimation = function(callback) {
+    var currentTime = 0;
+    var tOurTime = 0;
+    var tTheirTime = 0;
+    //
+    midi.clearAnimation();
+    ///
+    var frame = function() {
+      midi.animationFrameId = requestAnimationFrame(frame);
+      ///
+      if (midi.endTime === 0) {
+        return;
+      }
+      if (midi.playing) {
+        currentTime = (tTheirTime === midi.currentTime) ? tOurTime - Date.now() : 0;
+        if (midi.currentTime === 0) {
+          currentTime = 0;
+        } else {
+          currentTime = midi.currentTime - currentTime;
+        }
+        if (tTheirTime !== midi.currentTime) {
+          tOurTime = Date.now();
+          tTheirTime = midi.currentTime;
+        }
+      } else { // paused
+        currentTime = midi.currentTime;
+      }
+      ///
+      if(currentTime == 0 && midi.playing) currentTime = ((Date.now() - midi.ctxStartTime * 10) - midi.playingStartTime) / 100 * MIDI.Player.BPM;
+      if(midi.lastCallbackTime!=currentTime){
+        var endTime = midi.endTime;
+        //var percent = currentTime / endTime;
+        var t1 = currentTime / 1000;
+        var t2 = endTime / 1000;
+        ///
+        if (t2 - t1 < -1.0) {
+          return;
+        } else {
+          callback({
+            now: t1,
+            end: t2,
+            events: noteRegistrar
+          });
+        }
+        midi.lastCallbackTime = currentTime;
+      }
+    };
+    ///
+    requestAnimationFrame(frame);
+  };
+
+// helpers
+
+  midi.loadMidiFile = function(onsuccess, onprogress, onerror) {
+    try {
+      midi.replayer = new Replayer(MidiFile(midi.currentData), midi.timeWarp, null, midi.BPM);
+      midi.data = midi.replayer.getData();
+      midi.endTime = getLength();
+      ///
+      MIDI.loadPlugin({
+// 			instruments: midi.getFileInstruments(),
+        onsuccess: onsuccess,
+        onprogress: onprogress,
+        onerror: onerror
+      });
+    } catch(event) {
+      onerror && onerror(event);
+    }
+  };
+
+  midi.loadFile = function(file, onsuccess, onprogress, onerror) {
+    midi.stop();
+    if (file.indexOf('base64,') !== -1) {
+      var data = window.atob(file.split(',')[1]);
+      midi.currentData = data;
+      midi.loadMidiFile(onsuccess, onprogress, onerror);
+    } else {
+      var fetch = new XMLHttpRequest();
+      fetch.open('GET', file);
+      fetch.overrideMimeType('text/plain; charset=x-user-defined');
+      fetch.onreadystatechange = function() {
+        if (this.readyState === 4) {
+          if (this.status === 200) {
+            var t = this.responseText || '';
+            var ff = [];
+            var mx = t.length;
+            var scc = String.fromCharCode;
+            for (var z = 0; z < mx; z++) {
+              ff[z] = scc(t.charCodeAt(z) & 255);
+            }
+            ///
+            var data = ff.join('');
+            midi.currentData = data;
+            midi.loadMidiFile(onsuccess, onprogress, onerror);
+          } else {
+            onerror && onerror('Unable to load MIDI file');
+          }
+        }
+      };
+      fetch.send();
+    }
+  };
+
+  midi.getFileInstruments = function() {
+    var instruments = {};
+    var programs = {};
+    for (var n = 0; n < midi.data.length; n ++) {
+      var event = midi.data[n][0].event;
+      if (event.type !== 'channel') {
+        continue;
+      }
+      var channel = event.channel;
+      switch(event.subtype) {
+        case 'controller':
+//				console.log(event.channel, MIDI.defineControl[event.controllerType], event.value);
+          break;
+        case 'programChange':
+          programs[channel] = event.programNumber;
+          break;
+        case 'noteOn':
+          var program = programs[channel];
+          var gm = MIDI.GM.byId[isFinite(program) ? program : channel];
+          instruments[gm.id] = true;
+          break;
+      }
+    }
+    var ret = [];
+    for (var key in instruments) {
+      ret.push(key);
+    }
+    return ret;
+  };
+
+// Playing the audio
+
+  var eventQueue = []; // hold events to be triggered
+  var queuedTime; //
+  var startTime = 0; // to measure time elapse
+  var noteRegistrar = {}; // get event for requested note
+  var onMidiEvent = undefined; // listener
+  var scheduleTracking = function(channel, note, currentTime, offset, message, velocity, time) {
+    return setTimeout(function() {
+      var data = {
+        channel: channel,
+        note: note,
+        now: currentTime,
+        end: midi.endTime,
+        message: message,
+        velocity: velocity
+      };
+      //
+      if (message === 128) {
+        delete noteRegistrar[note];
+      } else {
+        noteRegistrar[note] = data;
+      }
+      if (onMidiEvent) {
+        onMidiEvent(data);
+      }
+      midi.currentTime = currentTime;
+      ///
+      eventQueue.shift();
+      ///
+      if (eventQueue.length < 1000) {
+        startAudio(queuedTime, true);
+      } else if (midi.currentTime === queuedTime && queuedTime < midi.endTime) { // grab next sequence
+        startAudio(queuedTime, true);
+      }
+    }, currentTime - offset);
+  };
+
+  var getContext = function() {
+    if (MIDI.api === 'webaudio') {
+      return MIDI.WebAudio.getContext();
+    } else {
+      midi.ctx = {currentTime: 0};
+    }
+    return midi.ctx;
+  };
+
+  var getLength = function() {
+    var data =  midi.data;
+    var length = data.length;
+    var totalTime = 0.5;
+    for (var n = 0; n < length; n++) {
+      totalTime += data[n][1];
+    }
+    return totalTime;
+  };
+
+  var __now;
+  var getNow = function() {
+    if (window.performance && window.performance.now) {
+      return window.performance.now();
+    } else {
+      return Date.now();
+    }
+  };
+
+  var startAudio = function(currentTime, fromCache, onsuccess) {
+    if (!midi.replayer) {
+      return;
+    }
+    if (!fromCache) {
+      if (typeof currentTime === 'undefined') {
+        currentTime = midi.restart;
+      }
+      ///
+      midi.playing && stopAudio();
+      midi.playing = true;
+      midi.data = midi.replayer.getData();
+      midi.endTime = getLength();
+    }
+    ///
+    var note;
+    var offset = 0;
+    var messages = 0;
+    var data = midi.data;
+    var ctx = getContext();
+    var length = data.length;
+    //
+    queuedTime = 0.5;
+    ///
+    var interval = eventQueue[0] && eventQueue[0].interval || 0;
+    var foffset = currentTime - midi.currentTime;
+    ///
+    if (MIDI.api !== 'webaudio') { // set currentTime on ctx
+      var now = getNow();
+      __now = __now || now;
+      ctx.currentTime = (now - __now) / 1000;
+    }
+    ///
+    midi.ctxStartTime = startTime = ctx.currentTime;
+    midi.playingStartTime = Date.now() - midi.ctxStartTime*10 ;
+    ///
+    for (var n = 0; n < length && messages < 100; n++) {
+      var obj = data[n];
+      if ((queuedTime += obj[1]) <= currentTime) {
+        offset = queuedTime;
+
+        if (obj[0].event.type !== 'channel')
+          continue;
+      }
+      ///
+      currentTime = queuedTime - offset;
+      ///
+      var event = obj[0].event;
+      if (event.type !== 'channel') {
+        continue;
+      }
+      ///
+      var channelId = event.channel;
+      var channel = MIDI.channels[channelId];
+      var delay = ctx.currentTime + ((currentTime + foffset + midi.startDelay) / 1000);
+      var queueTime = queuedTime - offset + midi.startDelay;
+      switch (event.subtype) {
+        case 'controller':
+          MIDI.setController(channelId, event.controllerType, event.value, delay);
+          break;
+        case 'programChange':
+          MIDI.programChange(channelId, event.programNumber, delay);
+          break;
+        case 'pitchBend':
+          MIDI.pitchBend(channelId, event.value, delay);
+          break;
+        case 'noteOn':
+          if (channel.mute) break;
+          note = event.noteNumber - (midi.MIDIOffset || 0);
+          eventQueue.push({
+            event: event,
+            time: queueTime,
+            source: MIDI.noteOn(channelId, event.noteNumber, event.velocity, delay),
+            interval: scheduleTracking(channelId, note, queuedTime + midi.startDelay, offset - foffset, 144, event.velocity)
+          });
+          messages++;
+          break;
+        case 'noteOff':
+          if (channel.mute) break;
+          note = event.noteNumber - (midi.MIDIOffset || 0);
+          eventQueue.push({
+            event: event,
+            time: queueTime,
+            source: MIDI.noteOff(channelId, event.noteNumber, delay),
+            interval: scheduleTracking(channelId, note, queuedTime, offset - foffset, 128, 0)
+          });
+          break;
+        default:
+          break;
+      }
+    }
+    ///
+    onsuccess && onsuccess(eventQueue);
+  };
+
+  var stopAudio = function() {
+    var ctx = getContext();
+    midi.playing = false;
+    midi.restart += (ctx.currentTime - startTime) * 1000;
+    // stop the audio, and intervals
+    while (eventQueue.length) {
+      var o = eventQueue.pop();
+      window.clearInterval(o.interval);
+      if (!o.source) continue; // is not webaudio
+      if (typeof(o.source) === 'number') {
+        window.clearTimeout(o.source);
+      } else { // webaudio
+        o.source.disconnect(0);
+      }
+    }
+    // run callback to cancel any notes still playing
+    for (var key in noteRegistrar) {
+      var o = noteRegistrar[key]
+      if (noteRegistrar[key].message === 144 && onMidiEvent) {
+        onMidiEvent({
+          channel: o.channel,
+          note: o.note,
+          now: o.now,
+          end: o.end,
+          message: 128,
+          velocity: o.velocity
+        });
+      }
+    }
+    // reset noteRegistrar
+    noteRegistrar = {};
+  };
+
+})();

+ 150 - 0
js/midi/plugin.audiotag.js

@@ -0,0 +1,150 @@
+/*
+	----------------------------------------------------------------------
+	AudioTag <audio> - OGG or MPEG Soundbank
+	----------------------------------------------------------------------
+	http://dev.w3.org/html5/spec/Overview.html#the-audio-element
+	----------------------------------------------------------------------
+*/
+
+(function(root) { 'use strict';
+
+	window.Audio && (function() {
+		var midi = root.AudioTag = { api: 'audiotag' };
+		var noteToKey = {};
+		var volume = 127; // floating point 
+		var buffer_nid = -1; // current channel
+		var audioBuffers = []; // the audio channels
+		var notesOn = []; // instrumentId + noteId that is currently playing in each 'channel', for routing noteOff/chordOff calls
+		var notes = {}; // the piano keys
+		for (var nid = 0; nid < 12; nid ++) {
+			audioBuffers[nid] = new Audio();
+		}
+
+		var playChannel = function(channel, note) {
+			if (!root.channels[channel]) return;
+			var instrument = root.channels[channel].instrument;
+			var instrumentId = root.GM.byId[instrument].id;
+			var note = notes[note];
+			if (note) {
+				var instrumentNoteId = instrumentId + '' + note.id;
+				var nid = (buffer_nid + 1) % audioBuffers.length;
+				var audio = audioBuffers[nid];
+				notesOn[ nid ] = instrumentNoteId;
+				if (!root.Soundfont[instrumentId]) {
+					if (root.DEBUG) {
+						console.log('404', instrumentId);
+					}
+					return;
+				}
+				audio.src = root.Soundfont[instrumentId][note.id];
+				audio.volume = volume / 127;
+				audio.play();
+				buffer_nid = nid;
+			}
+		};
+
+		var stopChannel = function(channel, note) {
+			if (!root.channels[channel]) return;
+			var instrument = root.channels[channel].instrument;
+			var instrumentId = root.GM.byId[instrument].id;
+			var note = notes[note];
+			if (note) {
+				var instrumentNoteId = instrumentId + '' + note.id;
+				for (var i = 0, len = audioBuffers.length; i < len; i++) {
+				    var nid = (i + buffer_nid + 1) % len;
+				    var cId = notesOn[nid];
+				    if (cId && cId == instrumentNoteId) {
+				        audioBuffers[nid].pause();
+				        notesOn[nid] = null;
+				        return;
+				    }
+				}
+			}
+		};
+	
+		midi.audioBuffers = audioBuffers;
+		midi.send = function(data, delay) { };
+		midi.setController = function(channel, type, value, delay) { };
+		midi.setVolume = function(channel, n) {
+			volume = n; //- should be channel specific volume
+		};
+
+		midi.programChange = function(channel, program) {
+			root.channels[channel].instrument = program;
+		};
+
+		midi.pitchBend = function(channel, program, delay) { };
+
+		midi.noteOn = function(channel, note, velocity, delay) {
+			var id = noteToKey[note];
+			if (!notes[id]) return;
+			if (delay) {
+				return setTimeout(function() {
+					playChannel(channel, id);
+				}, delay * 1000);
+			} else {
+				playChannel(channel, id);
+			}
+		};
+	
+		midi.noteOff = function(channel, note, delay) {
+// 			var id = noteToKey[note];
+// 			if (!notes[id]) return;
+// 			if (delay) {
+// 				return setTimeout(function() {
+// 					stopChannel(channel, id);
+// 				}, delay * 1000)
+// 			} else {
+// 				stopChannel(channel, id);
+// 			}
+		};
+	
+		midi.chordOn = function(channel, chord, velocity, delay) {
+			for (var idx = 0; idx < chord.length; idx ++) {
+				var n = chord[idx];
+				var id = noteToKey[n];
+				if (!notes[id]) continue;
+				if (delay) {
+					return setTimeout(function() {
+						playChannel(channel, id);
+					}, delay * 1000);
+				} else {
+					playChannel(channel, id);
+				}
+			}
+		};
+	
+		midi.chordOff = function(channel, chord, delay) {
+			for (var idx = 0; idx < chord.length; idx ++) {
+				var n = chord[idx];
+				var id = noteToKey[n];
+				if (!notes[id]) continue;
+				if (delay) {
+					return setTimeout(function() {
+						stopChannel(channel, id);
+					}, delay * 1000);
+				} else {
+					stopChannel(channel, id);
+				}
+			}
+		};
+	
+		midi.stopAllNotes = function() {
+			for (var nid = 0, length = audioBuffers.length; nid < length; nid++) {
+				audioBuffers[nid].pause();
+			}
+		};
+	
+		midi.connect = function(opts) {
+			root.setDefaultPlugin(midi);
+			///
+			for (var key in root.keyToNote) {
+				noteToKey[root.keyToNote[key]] = key;
+				notes[key] = {id: key};
+			}
+			///
+			opts.onsuccess && opts.onsuccess();
+		};
+	})();
+
+})(MIDI);

+ 326 - 0
js/midi/plugin.webaudio.js

@@ -0,0 +1,326 @@
+/*
+	----------------------------------------------------------
+	Web Audio API - OGG or MPEG Soundbank
+	----------------------------------------------------------
+	http://webaudio.github.io/web-audio-api/
+	----------------------------------------------------------
+*/
+
+(function(root) { 'use strict';
+
+	window.AudioContext && (function() {
+		var audioContext = null; // new AudioContext();
+		var useStreamingBuffer = false; // !!audioContext.createMediaElementSource;
+		var midi = root.WebAudio = {api: 'webaudio'};
+		var ctx; // audio context
+		var sources = {};
+		var effects = {};
+		var masterVolume = 127;
+		var audioBuffers = {};
+		///
+		midi.audioBuffers = audioBuffers;
+		midi.send = function(data, delay) { };
+		midi.setController = function(channelId, type, value, delay) { };
+
+		midi.setVolume = function(channelId, volume, delay) {
+			if (delay) {
+				setTimeout(function() {
+					masterVolume = volume;
+				}, delay * 1000);
+			} else {
+				masterVolume = volume;
+			}
+		};
+
+		midi.programChange = function(channelId, program, delay) {
+// 			if (delay) {
+// 				return setTimeout(function() {
+// 					var channel = root.channels[channelId];
+// 					channel.instrument = program;
+// 				}, delay);
+// 			} else {
+				var channel = root.channels[channelId];
+				channel.instrument = program;
+// 			}
+		};
+
+		midi.pitchBend = function(channelId, program, delay) {
+// 			if (delay) {
+// 				setTimeout(function() {
+// 					var channel = root.channels[channelId];
+// 					channel.pitchBend = program;
+// 				}, delay);
+// 			} else {
+				var channel = root.channels[channelId];
+				channel.pitchBend = program;
+// 			}
+		};
+
+		midi.noteOn = function(channelId, noteId, velocity, delay) {
+			delay = delay || 0;
+
+			/// check whether the note exists
+			var channel = root.channels[channelId];
+			var instrument = channel.instrument;
+			var bufferId = instrument + '' + noteId;
+			var buffer = audioBuffers[bufferId];
+			if (!buffer) {
+// 				console.log(MIDI.GM.byId[instrument].id, instrument, channelId);
+				return;
+			}
+
+			/// convert relative delay to absolute delay
+			if (delay < ctx.currentTime) {
+				delay += ctx.currentTime;
+			}
+		
+			/// create audio buffer
+			if (useStreamingBuffer) {
+				var source = ctx.createMediaElementSource(buffer);
+			} else { // XMLHTTP buffer
+				var source = ctx.createBufferSource();
+				source.buffer = buffer;
+			}
+
+			/// add effects to buffer
+			if (effects) {
+				var chain = source;
+				for (var key in effects) {
+					chain.connect(effects[key].input);
+					chain = effects[key];
+				}
+			}
+
+			/// add gain + pitchShift
+			var gain = (velocity / 127) * (masterVolume / 127) * 2 - 1;
+			source.connect(ctx.destination);
+			source.playbackRate.value = 1; // pitch shift 
+			source.gainNode = ctx.createGain(); // gain
+			source.gainNode.connect(ctx.destination);
+			source.gainNode.gain.value = Math.min(1.0, Math.max(-1.0, gain));
+			source.connect(source.gainNode);
+			///
+			if (useStreamingBuffer) {
+				if (delay) {
+					return setTimeout(function() {
+						buffer.currentTime = 0;
+						buffer.play()
+					}, delay * 1000);
+				} else {
+					buffer.currentTime = 0;
+					buffer.play()
+				}
+			} else {
+				source.start(delay || 0);
+			}
+			///
+			sources[channelId + '' + noteId] = source;
+			///
+			return source;
+		};
+
+		midi.noteOff = function(channelId, noteId, delay) {
+			delay = delay || 0;
+
+			/// check whether the note exists
+			var channel = root.channels[channelId];
+			var instrument = channel.instrument;
+			var bufferId = instrument + '' + noteId;
+			var buffer = audioBuffers[bufferId];
+			if (buffer) {
+				if (delay < ctx.currentTime) {
+					delay += ctx.currentTime;
+				}
+				///
+				var source = sources[channelId + '' + noteId];
+				if (source) {
+					if (source.gainNode) {
+						// @Miranet: 'the values of 0.2 and 0.3 could of course be used as 
+						// a 'release' parameter for ADSR like time settings.'
+						// add { 'metadata': { release: 0.3 } } to soundfont files
+						var gain = source.gainNode.gain;
+						gain.linearRampToValueAtTime(gain.value, delay);
+						gain.linearRampToValueAtTime(-1.0, delay + 0.3);
+					}
+					///
+					if (useStreamingBuffer) {
+						if (delay) {
+							setTimeout(function() {
+								buffer.pause();
+							}, delay * 1000);
+						} else {
+							buffer.pause();
+						}
+					} else {
+						if (source.noteOff) {
+							source.noteOff(delay + 0.5);
+						} else {
+							source.stop(delay + 0.5);
+						}
+					}
+					///
+					delete sources[channelId + '' + noteId];
+					///
+					return source;
+				}
+			}
+		};
+
+		midi.chordOn = function(channel, chord, velocity, delay) {
+			var res = {};
+			for (var n = 0, note, len = chord.length; n < len; n++) {
+				res[note = chord[n]] = midi.noteOn(channel, note, velocity, delay);
+			}
+			return res;
+		};
+
+		midi.chordOff = function(channel, chord, delay) {
+			var res = {};
+			for (var n = 0, note, len = chord.length; n < len; n++) {
+				res[note = chord[n]] = midi.noteOff(channel, note, delay);
+			}
+			return res;
+		};
+
+		midi.stopAllNotes = function() {
+			for (var sid in sources) {
+				var delay = 0;
+				if (delay < ctx.currentTime) {
+					delay += ctx.currentTime;
+				}
+				var source = sources[sid];
+				source.gain.linearRampToValueAtTime(1, delay);
+				source.gain.linearRampToValueAtTime(0, delay + 0.3);
+				if (source.noteOff) { // old api
+					source.noteOff(delay + 0.3);
+				} else { // new api
+					source.stop(delay + 0.3);
+				}
+				delete sources[sid];
+			}
+		};
+
+		midi.setEffects = function(list) {
+			if (ctx.tunajs) {
+				for (var n = 0; n < list.length; n ++) {
+					var data = list[n];
+					var effect = new ctx.tunajs[data.type](data);
+					effect.connect(ctx.destination);
+					effects[data.type] = effect;
+				}
+			} else {
+				return console.log('Effects module not installed.');
+			}
+		};
+
+		midi.connect = function(opts) {
+			root.setDefaultPlugin(midi);
+			midi.setContext(ctx || createAudioContext(), opts.onsuccess);
+		};
+	
+		midi.getContext = function() {
+			return ctx;
+		};
+	
+		midi.setContext = function(newCtx, onload, onprogress, onerror) {
+			ctx = newCtx;
+
+			/// tuna.js effects module - https://github.com/Dinahmoe/tuna
+			if (typeof Tuna !== 'undefined' && !ctx.tunajs) {
+				ctx.tunajs = new Tuna(ctx);
+			}
+		
+			/// loading audio files
+			var urls = [];
+			var notes = root.keyToNote;
+			for (var key in notes) urls.push(key);
+			///
+			var waitForEnd = function(instrument) {
+				for (var key in bufferPending) { // has pending items
+					if (bufferPending[key]) return;
+				}
+				///
+				if (onload) { // run onload once
+					onload();
+					onload = null;
+				}
+			};
+			///
+			var requestAudio = function(soundfont, instrumentId, index, key) {
+				var url = soundfont[key];
+				if (url) {
+					bufferPending[instrumentId] ++;
+					loadAudio(url, function(buffer) {
+						buffer.id = key;
+						var noteId = root.keyToNote[key];
+						audioBuffers[instrumentId + '' + noteId] = buffer;
+						///
+						if (-- bufferPending[instrumentId] === 0) {
+							var percent = index / 87;
+// 							console.log(MIDI.GM.byId[instrumentId], 'processing: ', percent);
+							soundfont.isLoaded = true;
+							waitForEnd(instrument);
+						}
+					}, function(err) {
+		// 				console.log(err);
+					});
+				}
+			};
+			///
+			var bufferPending = {};
+			for (var instrument in root.Soundfont) {
+				var soundfont = root.Soundfont[instrument];
+				if (soundfont.isLoaded) {
+					continue;
+				}
+				///
+				var synth = root.GM.byName[instrument];
+				var instrumentId = synth.number;
+				///
+				bufferPending[instrumentId] = 0;
+				///
+				for (var index = 0; index < urls.length; index++) {
+					var key = urls[index];
+					requestAudio(soundfont, instrumentId, index, key);
+				}
+			}
+			///
+			setTimeout(waitForEnd, 1);
+		};
+
+		/* Load audio file: streaming | base64 | arraybuffer
+		---------------------------------------------------------------------- */
+		function loadAudio(url, onload, onerror) {
+			if (useStreamingBuffer) {
+				var audio = new Audio();
+				audio.src = url;
+				audio.controls = false;
+				audio.autoplay = false;
+				audio.preload = false;
+				audio.addEventListener('canplay', function() {
+					onload && onload(audio);
+				});
+				audio.addEventListener('error', function(err) {
+					onerror && onerror(err);
+				});
+				document.body.appendChild(audio);
+			} else if (url.indexOf('data:audio') === 0) { // Base64 string
+				var base64 = url.split(',')[1];
+				var buffer = Base64Binary.decodeArrayBuffer(base64);
+				ctx.decodeAudioData(buffer, onload, onerror);
+			} else { // XMLHTTP buffer
+				var request = new XMLHttpRequest();
+				request.open('GET', url, true);
+				request.responseType = 'arraybuffer';
+				request.onload = function() {
+					ctx.decodeAudioData(request.response, onload, onerror);
+				};
+				request.send();
+			}
+		};
+		
+		function createAudioContext() {
+			return new (window.AudioContext || window.webkitAudioContext)();
+		};
+	})();
+})(MIDI);

+ 93 - 0
js/midi/plugin.webmidi.js

@@ -0,0 +1,93 @@
+/*
+	----------------------------------------------------------------------
+	Web MIDI API - Native Soundbanks
+	----------------------------------------------------------------------
+	http://webaudio.github.io/web-midi-api/
+	----------------------------------------------------------------------
+*/
+
+(function(root) { 'use strict';
+
+	var plugin = null;
+	var output = null;
+	var channels = [];
+	var midi = root.WebMIDI = {api: 'webmidi'};
+	midi.send = function(data, delay) { // set channel volume
+		output.send(data, delay * 1000);
+	};
+
+	midi.setController = function(channel, type, value, delay) {
+		output.send([channel, type, value], delay * 1000);
+	};
+
+	midi.setVolume = function(channel, volume, delay) { // set channel volume
+		output.send([0xB0 + channel, 0x07, volume], delay * 1000);
+	};
+
+	midi.programChange = function(channel, program, delay) { // change patch (instrument)
+		output.send([0xC0 + channel, program], delay * 1000);
+	};
+
+	midi.pitchBend = function(channel, program, delay) { // pitch bend
+		output.send([0xE0 + channel, program], delay * 1000);
+	};
+
+	midi.noteOn = function(channel, note, velocity, delay) {
+		output.send([0x90 + channel, note, velocity], delay * 1000);
+	};
+
+	midi.noteOff = function(channel, note, delay) {
+		output.send([0x80 + channel, note, 0], delay * 1000);
+	};
+
+	midi.chordOn = function(channel, chord, velocity, delay) {
+		for (var n = 0; n < chord.length; n ++) {
+			var note = chord[n];
+			output.send([0x90 + channel, note, velocity], delay * 1000);
+		}
+	};
+
+	midi.chordOff = function(channel, chord, delay) {
+		for (var n = 0; n < chord.length; n ++) {
+			var note = chord[n];
+			output.send([0x80 + channel, note, 0], delay * 1000);
+		}
+	};
+
+	midi.stopAllNotes = function() {
+		output.cancel();
+		for (var channel = 0; channel < 16; channel ++) {
+			output.send([0xB0 + channel, 0x7B, 0]);
+		}
+	};
+
+	midi.connect = function(opts) {
+		root.setDefaultPlugin(midi);
+		var errFunction = function(err) { // well at least we tried!
+			if (window.AudioContext) { // Chrome
+				opts.api = 'webaudio';
+			} else if (window.Audio) { // Firefox
+				opts.api = 'audiotag';
+			} else { // no support
+				return;
+			}
+			root.loadPlugin(opts);
+		};
+		///
+		navigator.requestMIDIAccess().then(function(access) {
+			plugin = access;
+			var pluginOutputs = plugin.outputs;
+			if (typeof pluginOutputs == 'function') { // Chrome pre-43
+				output = pluginOutputs()[0];
+			} else { // Chrome post-43
+				output = pluginOutputs[0];
+			}
+			if (output === undefined) { // nothing there...
+				errFunction();
+			} else {
+				opts.onsuccess && opts.onsuccess();			
+			}
+		}, errFunction);
+	};
+
+})(MIDI);

+ 320 - 0
js/midi/synesthesia.js

@@ -0,0 +1,320 @@
+/*
+	----------------------------------------------------------
+	MIDI.Synesthesia : 0.3.1 : 2012-01-06
+	----------------------------------------------------------
+	Peacock:  “Instruments to perform color-music: Two centuries of technological experimentation,” Leonardo, 21 (1988), 397-406.
+	Gerstner:  Karl Gerstner, The Forms of Color 1986
+	Klein:  Colour-Music: The art of light, London: Crosby Lockwood and Son, 1927.
+	Jameson:  “Visual music in a visual programming language,” IEEE Symposium on Visual Languages, 1999, 111-118. 
+	Helmholtz:  Treatise on Physiological Optics, New York: Dover Books, 1962 
+	Jones:  The art of light & color, New York: Van Nostrand Reinhold, 1972
+	----------------------------------------------------------
+	Reference: http://rhythmiclight.com/archives/ideas/colorscales.html
+	----------------------------------------------------------
+*/
+
+if (typeof MIDI === 'undefined') var MIDI = {};
+
+MIDI.Synesthesia = MIDI.Synesthesia || {};
+
+(function(root) {
+	root.data = {
+		'Isaac Newton (1704)': { 
+			format: 'HSL',
+			ref: 'Gerstner, p.167',
+			english: ['red',null,'orange',null,'yellow','green',null,'blue',null,'indigo',null,'violet'],
+			0: [ 0, 96, 51 ], // C
+			1: [ 0, 0, 0 ], // C#
+			2: [ 29, 94, 52 ], // D
+			3: [ 0, 0, 0 ], // D#
+			4: [ 60, 90, 60 ], // E
+			5: [ 135, 76, 32 ], // F
+			6: [ 0, 0, 0 ], // F#
+			7: [ 248, 82, 28 ], // G
+			8: [ 0, 0, 0 ], // G#
+			9: [ 302, 88, 26 ], // A
+			10: [ 0, 0, 0 ], // A#
+			11: [ 325, 84, 46 ] // B
+		},
+		'Louis Bertrand Castel (1734)': { 
+			format: 'HSL',
+			ref: 'Peacock, p.400',
+			english: ['blue','blue-green','green','olive green','yellow','yellow-orange','orange','red','crimson','violet','agate','indigo'],
+			0: [ 248, 82, 28 ],
+			1: [ 172, 68, 34 ],
+			2: [ 135, 76, 32 ],
+			3: [ 79, 59, 36 ],
+			4: [ 60, 90, 60 ],
+			5: [ 49, 90, 60 ],
+			6: [ 29, 94, 52 ],
+			7: [ 360, 96, 51 ],
+			8: [ 1, 89, 33 ],
+			9: [ 325, 84, 46 ],
+			10: [ 273, 80, 27 ],
+			11: [ 302, 88, 26 ]
+		},
+		'George Field (1816)': { 
+			format: 'HSL',
+			ref: 'Klein, p.69',
+			english: ['blue',null,'purple',null,'red','orange',null,'yellow',null,'yellow green',null,'green'],
+			0: [ 248, 82, 28 ],
+			1: [ 0, 0, 0 ],
+			2: [ 302, 88, 26 ],
+			3: [ 0, 0, 0 ],
+			4: [ 360, 96, 51 ],
+			5: [ 29, 94, 52 ],
+			6: [ 0, 0, 0 ],
+			7: [ 60, 90, 60 ],
+			8: [ 0, 0, 0 ],
+			9: [ 79, 59, 36 ],
+			10: [ 0, 0, 0 ],
+			11: [ 135, 76, 32 ]
+		},
+		'D. D. Jameson (1844)': { 
+			format: 'HSL',
+			ref: 'Jameson, p.12',
+			english: ['red','red-orange','orange','orange-yellow','yellow','green','green-blue','blue','blue-purple','purple','purple-violet','violet'],
+			0: [ 360, 96, 51 ],
+			1: [ 14, 91, 51 ],
+			2: [ 29, 94, 52 ],
+			3: [ 49, 90, 60 ],
+			4: [ 60, 90, 60 ],
+			5: [ 135, 76, 32 ],
+			6: [ 172, 68, 34 ],
+			7: [ 248, 82, 28 ],
+			8: [ 273, 80, 27 ],
+			9: [ 302, 88, 26 ],
+			10: [ 313, 78, 37 ],
+			11: [ 325, 84, 46 ]
+		},
+		'Theodor Seemann (1881)': { 
+			format: 'HSL',
+			ref: 'Klein, p.86',
+			english: ['carmine','scarlet','orange','yellow-orange','yellow','green','green blue','blue','indigo','violet','brown','black'],
+			0: [ 0, 58, 26 ],
+			1: [ 360, 96, 51 ],
+			2: [ 29, 94, 52 ],
+			3: [ 49, 90, 60 ],
+			4: [ 60, 90, 60 ],
+			5: [ 135, 76, 32 ],
+			6: [ 172, 68, 34 ],
+			7: [ 248, 82, 28 ],
+			8: [ 302, 88, 26 ],
+			9: [ 325, 84, 46 ],
+			10: [ 0, 58, 26 ],
+			11: [ 0, 0, 3 ]
+		},
+		'A. Wallace Rimington (1893)': { 
+			format: 'HSL',
+			ref: 'Peacock, p.402',
+			english: ['deep red','crimson','orange-crimson','orange','yellow','yellow-green','green','blueish green','blue-green','indigo','deep blue','violet'],
+			0: [ 360, 96, 51 ],
+			1: [ 1, 89, 33 ],
+			2: [ 14, 91, 51 ],
+			3: [ 29, 94, 52 ],
+			4: [ 60, 90, 60 ],
+			5: [ 79, 59, 36 ],
+			6: [ 135, 76, 32 ],
+			7: [ 163, 62, 40 ],
+			8: [ 172, 68, 34 ],
+			9: [ 302, 88, 26 ],
+			10: [ 248, 82, 28 ],
+			11: [ 325, 84, 46 ]
+		},
+		'Bainbridge Bishop (1893)': { 
+			format: 'HSL',
+			ref: 'Bishop, p.11',
+			english: ['red','orange-red or scarlet','orange','gold or yellow-orange','yellow or green-gold','yellow-green','green','greenish-blue or aquamarine','blue','indigo or violet-blue','violet','violet-red','red'],
+			0: [ 360, 96, 51 ],
+			1: [ 1, 89, 33 ],
+			2: [ 29, 94, 52 ],
+			3: [ 50, 93, 52 ],
+			4: [ 60, 90, 60 ],
+			5: [ 73, 73, 55 ],
+			6: [ 135, 76, 32 ],
+			7: [ 163, 62, 40 ],
+			8: [ 302, 88, 26 ],
+			9: [ 325, 84, 46 ],
+			10: [ 343, 79, 47 ],
+			11: [ 360, 96, 51 ]
+		},
+		'H. von Helmholtz (1910)': { 
+			format: 'HSL',
+			ref: 'Helmholtz, p.22',
+			english: ['yellow','green','greenish blue','cayan-blue','indigo blue','violet','end of red','red','red','red','red orange','orange'],
+			0: [ 60, 90, 60 ],
+			1: [ 135, 76, 32 ],
+			2: [ 172, 68, 34 ],
+			3: [ 211, 70, 37 ],
+			4: [ 302, 88, 26 ],
+			5: [ 325, 84, 46 ],
+			6: [ 330, 84, 34 ],
+			7: [ 360, 96, 51 ],
+			8: [ 10, 91, 43 ],
+			9: [ 10, 91, 43 ],
+			10: [ 8, 93, 51 ],
+			11: [ 28, 89, 50 ]
+		},
+		'Alexander Scriabin (1911)': { 
+			format: 'HSL',
+			ref: 'Jones, p.104',
+			english: ['red','violet','yellow','steely with the glint of metal','pearly blue the shimmer of moonshine','dark red','bright blue','rosy orange','purple','green','steely with a glint of metal','pearly blue the shimmer of moonshine'],
+			0: [ 360, 96, 51 ],
+			1: [ 325, 84, 46 ],
+			2: [ 60, 90, 60 ],
+			3: [ 245, 21, 43 ],
+			4: [ 211, 70, 37 ],
+			5: [ 1, 89, 33 ],
+			6: [ 248, 82, 28 ],
+			7: [ 29, 94, 52 ],
+			8: [ 302, 88, 26 ],
+			9: [ 135, 76, 32 ],
+			10: [ 245, 21, 43 ],
+			11: [ 211, 70, 37 ]
+		},
+		'Adrian Bernard Klein (1930)': { 
+			format: 'HSL',
+			ref: 'Klein, p.209',
+			english: ['dark red','red','red orange','orange','yellow','yellow green','green','blue-green','blue','blue violet','violet','dark violet'],
+			0: [ 0, 91, 40 ],
+			1: [ 360, 96, 51 ],
+			2: [ 14, 91, 51 ],
+			3: [ 29, 94, 52 ],
+			4: [ 60, 90, 60 ],
+			5: [ 73, 73, 55 ],
+			6: [ 135, 76, 32 ],
+			7: [ 172, 68, 34 ],
+			8: [ 248, 82, 28 ],
+			9: [ 292, 70, 31 ],
+			10: [ 325, 84, 46 ],
+			11: [ 330, 84, 34 ]
+		},
+		'August Aeppli (1940)': { 
+			format: 'HSL',
+			ref: 'Gerstner, p.169',
+			english: ['red',null,'orange',null,'yellow',null,'green','blue-green',null,'ultramarine blue','violet','purple'],
+			0: [ 0, 96, 51 ],
+			1: [ 0, 0, 0 ],
+			2: [ 29, 94, 52 ],
+			3: [ 0, 0, 0 ],
+			4: [ 60, 90, 60 ],
+			5: [ 0, 0, 0 ],
+			6: [ 135, 76, 32 ],
+			7: [ 172, 68, 34 ],
+			8: [ 0, 0, 0 ],
+			9: [ 211, 70, 37 ],
+			10: [ 273, 80, 27 ],
+			11: [ 302, 88, 26 ]
+		},
+		'I. J. Belmont (1944)': { 
+			ref: 'Belmont, p.226',
+			english: ['red','red-orange','orange','yellow-orange','yellow','yellow-green','green','blue-green','blue','blue-violet','violet','red-violet'],
+			0: [ 360, 96, 51 ],
+			1: [ 14, 91, 51 ],
+			2: [ 29, 94, 52 ],
+			3: [ 50, 93, 52 ],
+			4: [ 60, 90, 60 ],
+			5: [ 73, 73, 55 ],
+			6: [ 135, 76, 32 ],
+			7: [ 172, 68, 34 ],
+			8: [ 248, 82, 28 ],
+			9: [ 313, 78, 37 ],
+			10: [ 325, 84, 46 ],
+			11: [ 338, 85, 37 ]
+		},
+		'Steve Zieverink (2004)': { 
+			format: 'HSL',
+			ref: 'Cincinnati Contemporary Art Center',
+			english: ['yellow-green','green','blue-green','blue','indigo','violet','ultra violet','infra red','red','orange','yellow-white','yellow'],
+			0: [ 73, 73, 55 ],
+			1: [ 135, 76, 32 ],
+			2: [ 172, 68, 34 ],
+			3: [ 248, 82, 28 ],
+			4: [ 302, 88, 26 ],
+			5: [ 325, 84, 46 ],
+			6: [ 326, 79, 24 ],
+			7: [ 1, 89, 33 ],
+			8: [ 360, 96, 51 ],
+			9: [ 29, 94, 52 ],
+			10: [ 62, 78, 74 ],
+			11: [ 60, 90, 60 ]
+		},
+		'Circle of Fifths (Johnston 2003)': {
+			format: 'RGB',
+			ref: 'Joseph Johnston',
+			english: ['yellow', 'blue', 'orange', 'teal', 'red', 'green', 'purple', 'light orange', 'light blue', 'dark orange', 'dark green', 'violet' ],
+			0: [ 255, 255, 0 ],
+			1: [ 50, 0, 255 ],
+			2: [ 255, 150, 0 ],
+			3: [ 0, 210, 180 ],
+			4: [ 255, 0, 0 ],
+			5: [ 130, 255, 0 ],
+			6: [ 150, 0, 200 ],
+			7: [ 255, 195, 0 ],
+			8: [ 30, 130, 255 ],
+			9: [ 255, 100, 0 ],
+			10: [ 0, 200, 0 ],
+			11: [ 225, 0, 225 ]
+		},
+		'Circle of Fifths (Wheatman 2002)': {
+			format: 'HEX',
+			ref: 'Stuart Wheatman', // http://www.valleysfamilychurch.org/
+			english: [],
+			data: ['#122400', '#2E002E', '#002914', '#470000', '#002142', '#2E2E00', '#290052', '#003D00', '#520029', '#003D3D', '#522900', '#000080', '#244700', '#570057', '#004D26', '#7A0000', '#003B75', '#4C4D00', '#47008F', '#006100', '#850042', '#005C5C', '#804000', '#0000C7', '#366B00', '#80007F', '#00753B', '#B80000', '#0057AD', '#6B6B00', '#6600CC', '#008A00', '#B8005C', '#007F80', '#B35900', '#2424FF', '#478F00', '#AD00AD', '#00994D', '#F00000', '#0073E6', '#8F8F00', '#8A14FF', '#00AD00', '#EB0075', '#00A3A3', '#E07000', '#6B6BFF', '#5CB800', '#DB00DB', '#00C261', '#FF5757', '#3399FF', '#ADAD00', '#B56BFF', '#00D600', '#FF57AB', '#00C7C7', '#FF9124', '#9999FF', '#6EDB00', '#FF29FF', '#00E070', '#FF9999', '#7ABDFF', '#D1D100', '#D1A3FF', '#00FA00', '#FFA3D1', '#00E5E6', '#FFC285', '#C2C2FF', '#80FF00', '#FFA8FF', '#00E070', '#FFCCCC', '#C2E0FF', '#F0F000', '#EBD6FF', '#ADFFAD', '#FFD6EB', '#8AFFFF', '#FFEBD6', '#EBEBFF', '#E0FFC2', '#FFEBFF', '#E5FFF2', '#FFF5F5']		}
+	};
+
+	root.map = function(type) {
+		var data = {};
+		var blend = function(a, b) {
+			return [ // blend two colors and round results
+				(a[0] * 0.5 + b[0] * 0.5 + 0.5) >> 0, 
+				(a[1] * 0.5 + b[1] * 0.5 + 0.5) >> 0,
+				(a[2] * 0.5 + b[2] * 0.5 + 0.5) >> 0
+			];
+		};
+		///
+		var syn = root.data;
+		var colors = syn[type] || syn['D. D. Jameson (1844)'];
+		for (var note = 0, pclr, H, S, L; note <= 88; note ++) { // creates mapping for 88 notes
+			if (colors.data) {
+				data[note] = {
+					hsl: colors.data[note],
+					hex: colors.data[note] 
+				};
+			} else {
+				var clr = colors[(note + 9) % 12];
+				///
+				switch(colors.format) {
+					case 'RGB':
+						clr = Color.Space(clr, 'RGB>HSL');
+						H = clr.H >> 0;
+						S = clr.S >> 0;
+						L = clr.L >> 0;
+						break;
+					case 'HSL':
+						H = clr[0];
+						S = clr[1];
+						L = clr[2];
+						break;
+				}
+				///
+				if (H === S && S === L) { // note color is unset
+					clr = blend(pclr, colors[(note + 10) % 12]);
+				}
+				///
+// 				var amount = L / 10;
+// 				var octave = note / 12 >> 0;
+// 				var octaveLum = L + amount * octave - 3.0 * amount; // map luminance to octave
+				///
+				data[note] = {
+					hsl: 'hsla(' + H + ',' + S + '%,' + L + '%, 1)',
+					hex: Color.Space({H: H, S: S, L: L}, 'HSL>RGB>HEX>W3')
+				};
+				///
+				pclr = clr;
+			}
+		}
+		return data;
+	};
+
+})(MIDI.Synesthesia);

+ 225 - 0
js/util/dom_request_script.js

@@ -0,0 +1,225 @@
+/*
+	-----------------------------------------------------------
+	dom.loadScript.js : 0.1.4 : 2014/02/12 : http://mudcu.be
+	-----------------------------------------------------------
+	Copyright 2011-2014 Mudcube. All rights reserved.
+	-----------------------------------------------------------
+	/// No verification
+	dom.loadScript.add("../js/jszip/jszip.js");
+	/// Strict loading order and verification.
+	dom.loadScript.add({
+		strictOrder: true,
+		urls: [
+			{
+				url: "../js/jszip/jszip.js",
+				verify: "JSZip",
+				onsuccess: function() {
+					console.log(1)
+				}
+			},
+			{ 
+				url: "../inc/downloadify/js/swfobject.js",
+				verify: "swfobject",
+				onsuccess: function() {
+					console.log(2)
+				}
+			}
+		],
+		onsuccess: function() {
+			console.log(3)
+		}
+	});
+	/// Just verification.
+	dom.loadScript.add({
+		url: "../js/jszip/jszip.js",
+		verify: "JSZip",
+		onsuccess: function() {
+			console.log(1)
+		}
+	});
+*/
+
+if (typeof(dom) === "undefined") var dom = {};
+
+(function() { "use strict";
+
+dom.loadScript = function() {
+	this.loaded = {};
+	this.loading = {};
+	return this;
+};
+
+dom.loadScript.prototype.add = function(config) {
+	var that = this;
+	if (typeof(config) === "string") {
+		config = { url: config };
+	}
+	var urls = config.urls;
+	if (typeof(urls) === "undefined") {
+		urls = [{ 
+			url: config.url, 
+			verify: config.verify
+		}];
+	}
+	/// adding the elements to the head
+	var doc = document.getElementsByTagName("head")[0];
+	/// 
+	var testElement = function(element, test) {
+		if (that.loaded[element.url]) return;
+		if (test && globalExists(test) === false) return;
+		that.loaded[element.url] = true;
+		//
+		if (that.loading[element.url]) that.loading[element.url]();
+		delete that.loading[element.url];
+		//
+		if (element.onsuccess) element.onsuccess();
+		if (typeof(getNext) !== "undefined") getNext();
+	};
+	///
+	var hasError = false;
+	var batchTest = [];
+	var addElement = function(element) {
+		if (typeof(element) === "string") {
+			element = {
+				url: element,
+				verify: config.verify
+			};
+		}
+		if (/([\w\d.\[\]\'\"])$/.test(element.verify)) { // check whether its a variable reference
+			var verify = element.test = element.verify;
+			if (typeof(verify) === "object") {
+				for (var n = 0; n < verify.length; n ++) {
+					batchTest.push(verify[n]);
+				}			
+			} else {
+				batchTest.push(verify);
+			}
+		}
+		if (that.loaded[element.url]) return;
+		var script = document.createElement("script");
+		script.onreadystatechange = function() {
+			if (this.readyState !== "loaded" && this.readyState !== "complete") return;
+			testElement(element);
+		};
+		script.onload = function() {
+			testElement(element);
+		};
+		script.onerror = function() {
+			hasError = true;
+			delete that.loading[element.url];
+			if (typeof(element.test) === "object") {
+				for (var key in element.test) {
+					removeTest(element.test[key]);
+				}			
+			} else {
+				removeTest(element.test);
+			}
+		};
+		script.setAttribute("type", "text/javascript");
+		script.setAttribute("src", element.url);
+		doc.appendChild(script);
+		that.loading[element.url] = function() {};
+	};
+	/// checking to see whether everything loaded properly
+	var removeTest = function(test) {
+		var ret = [];
+		for (var n = 0; n < batchTest.length; n ++) {
+			if (batchTest[n] === test) continue;
+			ret.push(batchTest[n]);
+		}
+		batchTest = ret;
+	};
+	var onLoad = function(element) {
+		if (element) {
+			testElement(element, element.test);
+		} else {
+			for (var n = 0; n < urls.length; n ++) {
+				testElement(urls[n], urls[n].test);
+			}
+		}
+		var istrue = true;
+		for (var n = 0; n < batchTest.length; n ++) {
+			if (globalExists(batchTest[n]) === false) {
+				istrue = false;
+			}
+		}
+		if (!config.strictOrder && istrue) { // finished loading all the requested scripts
+			if (hasError) {
+				if (config.error) {
+					config.error();
+				}
+			} else if (config.onsuccess) {
+				config.onsuccess();
+			}
+		} else { // keep calling back the function
+			setTimeout(function() { //- should get slower over time?
+				onLoad(element);
+			}, 10);
+		}
+	};
+	/// loading methods;  strict ordering or loose ordering
+	if (config.strictOrder) {
+		var ID = -1;
+		var getNext = function() {
+			ID ++;
+			if (!urls[ID]) { // all elements are loaded
+				if (hasError) {
+					if (config.error) {
+						config.error();
+					}
+				} else if (config.onsuccess) {
+					config.onsuccess();
+				}
+			} else { // loading new script
+				var element = urls[ID];
+				var url = element.url;
+				if (that.loading[url]) { // already loading from another call (attach to event)
+					that.loading[url] = function() {
+						if (element.onsuccess) element.onsuccess();
+						getNext();
+					}
+				} else if (!that.loaded[url]) { // create script element
+					addElement(element);
+					onLoad(element);
+				} else { // it's already been successfully loaded
+					getNext();
+				}
+			}
+		};
+		getNext();
+	} else { // loose ordering
+		for (var ID = 0; ID < urls.length; ID ++) {
+			addElement(urls[ID]);
+			onLoad(urls[ID]);
+		}
+	}
+};
+
+dom.loadScript = new dom.loadScript();
+
+var globalExists = function(path, root) {
+	try {
+		path = path.split('"').join('').split("'").join('').split(']').join('').split('[').join('.');
+		var parts = path.split(".");
+		var length = parts.length;
+		var object = root || window;
+		for (var n = 0; n < length; n ++) {
+			var key = parts[n];
+			if (object[key] == null) {
+				return false;
+			} else { //
+				object = object[key];
+			}
+		}
+		return true;
+	} catch(e) {
+		return false;
+	}
+};
+
+})();
+
+/// For NodeJS
+if (typeof (module) !== "undefined" && module.exports) {
+	module.exports = dom.loadScript;
+}

+ 146 - 0
js/util/dom_request_xhr.js

@@ -0,0 +1,146 @@
+/*
+	----------------------------------------------------------
+	util/Request : 0.1.1 : 2015-03-26
+	----------------------------------------------------------
+	util.request({
+		url: './dir/something.extension',
+		data: 'test!',
+		format: 'text', // text | xml | json | binary
+		responseType: 'text', // arraybuffer | blob | document | json | text
+		headers: {},
+		withCredentials: true, // true | false
+		///
+		onerror: function(evt, percent) {
+			console.log(evt);
+		},
+		onsuccess: function(evt, responseText) {
+			console.log(responseText);
+		},
+		onprogress: function(evt, percent) {
+			percent = Math.round(percent * 100);
+			loader.create('thread', 'loading... ', percent);
+		}
+	});
+*/
+
+if (typeof MIDI === 'undefined') MIDI = {};
+
+(function(root) {
+
+	var util = root.util || (root.util = {});
+
+	util.request = function(opts, onsuccess, onerror, onprogress) { 'use strict';
+		if (typeof opts === 'string') opts = {url: opts};
+		///
+		var data = opts.data;
+		var url = opts.url;
+		var method = opts.method || (opts.data ? 'POST' : 'GET');
+		var format = opts.format;
+		var headers = opts.headers;
+		var responseType = opts.responseType;
+		var withCredentials = opts.withCredentials || false;
+		///
+		var onsuccess = onsuccess || opts.onsuccess;
+		var onerror = onerror || opts.onerror;
+		var onprogress = onprogress || opts.onprogress;
+		///
+		if (typeof NodeFS !== 'undefined' && root.loc.isLocalUrl(url)) {
+			NodeFS.readFile(url, 'utf8', function(err, res) {
+				if (err) {
+					onerror && onerror(err);
+				} else {
+					onsuccess && onsuccess({responseText: res});
+				}
+			});
+			return;
+		}
+		///
+		var xhr = new XMLHttpRequest();
+		xhr.open(method, url, true);
+		///
+		if (headers) {
+			for (var type in headers) {
+				xhr.setRequestHeader(type, headers[type]);
+			}
+		} else if (data) { // set the default headers for POST
+			xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+		}
+		if (format === 'binary') { //- default to responseType="blob" when supported
+			if (xhr.overrideMimeType) {
+				xhr.overrideMimeType('text/plain; charset=x-user-defined');
+			}
+		}
+		if (responseType) {
+			xhr.responseType = responseType;
+		}
+		if (withCredentials) {
+			xhr.withCredentials = 'true';
+		}
+		if (onerror && 'onerror' in xhr) {
+			xhr.onerror = onerror;
+		}
+		if (onprogress && xhr.upload && 'onprogress' in xhr.upload) {
+			if (data) {
+				xhr.upload.onprogress = function(evt) {
+					onprogress.call(xhr, evt, event.loaded / event.total);
+				};
+			} else {
+				xhr.addEventListener('progress', function(evt) {
+					var totalBytes = 0;
+					if (evt.lengthComputable) {
+						totalBytes = evt.total;
+					} else if (xhr.totalBytes) {
+						totalBytes = xhr.totalBytes;
+					} else {
+						var rawBytes = parseInt(xhr.getResponseHeader('Content-Length-Raw'));
+						if (isFinite(rawBytes)) {
+							xhr.totalBytes = totalBytes = rawBytes;
+						} else {
+							return;
+						}
+					}
+					onprogress.call(xhr, evt, evt.loaded / totalBytes);
+				});
+			}
+		}
+		///
+		xhr.onreadystatechange = function(evt) {
+			if (xhr.readyState === 4) { // The request is complete
+				if (xhr.status === 200 || // Response OK
+					xhr.status === 304 || // Not Modified
+					xhr.status === 308 || // Permanent Redirect
+					xhr.status === 0 && root.client.cordova // Cordova quirk
+				) {
+					if (onsuccess) {
+						var res;
+						if (format === 'xml') {
+							res = evt.target.responseXML;
+						} else if (format === 'text') {
+							res = evt.target.responseText;
+						} else if (format === 'json') {
+							try {
+								res = JSON.parse(evt.target.response);
+							} catch(err) {
+								onerror && onerror.call(xhr, evt);
+							}
+						}
+						///
+						onsuccess.call(xhr, evt, res);
+					}
+				} else {
+					onerror && onerror.call(xhr, evt);
+				}
+			}
+		};
+		xhr.send(data);
+		return xhr;
+	};
+
+	/// NodeJS
+	if (typeof module !== 'undefined' && module.exports) {
+		var NodeFS = require('fs');
+		XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
+		module.exports = root.util.request;
+	}
+
+})(MIDI);

File diff suppressed because it is too large
+ 0 - 0
midi.min.js


BIN
midis/AQUA.mid


BIN
midis/Again.mid


BIN
midis/All in good time.mid


BIN
midis/Allegro Cantabile Sound.mid


BIN
midis/Amnanesis.mid


BIN
midis/Arifutarekanashiminohate.mid


BIN
midis/Aruji Naki Sono Koe.mid


BIN
midis/Ashita e no Kaerimichi.mid


BIN
midis/Astronomia.mid


BIN
midis/Attack on Titan.mid


BIN
midis/Before my body is dry.mid


BIN
midis/Bios.mid


BIN
midis/Blessing.mid


BIN
midis/Blue Bird.mid


BIN
midis/Blumenkranz.mid


BIN
midis/Boku ja nai.mid


BIN
midis/Boys be Smile.mid


BIN
midis/Brave Heart.mid


BIN
midis/Brave Shine.mid


BIN
midis/Brave Song (piano + viola).mid


BIN
midis/Brave Song.mid


BIN
midis/Butterfly.mid


BIN
midis/COLORS.mid


BIN
midis/Cha-la Head-Cha-La.mid


BIN
midis/Challenge accepted (1).mid


BIN
midis/City of Eternity.mid


BIN
midis/Cras numquam scire.mid


BIN
midis/Daydream Syndrome.mid


BIN
midis/Dream theme from Nazo no Kanojo X.mid


BIN
midis/EONIAN.mid


BIN
midis/Enter Enter Mission.mid


BIN
midis/Esoragoto.mid


BIN
midis/Euterpe.mid


BIN
midis/Everyday World.mid


BIN
midis/Extra magic hour.mid


BIN
midis/Fallen Angel.mid


BIN
midis/Fubuki.mid


BIN
midis/GO GO Maniac.mid


BIN
midis/Gotta catch 'em all.mid


BIN
midis/Guren no Yumiya.mid


BIN
midis/Heartwarming.mid


BIN
midis/Hello Alone -Yui Ballade-.mid


BIN
midis/Hikari no Senritsu.mid


BIN
midis/Hikaru Nara.mid


BIN
midis/Jiyuu no Tsubasa for two pianos (MIDI).mid


BIN
midis/Kanashimi no ato ni.mid


BIN
midis/Kancolle - Piano Suite.mid


BIN
midis/Key anime piano suite.mid


BIN
midis/Kibou ni Tsuite.mid


BIN
midis/Kibou no Hana.mid


BIN
midis/Killy Killy Joker.mid


BIN
midis/Kimi ni Matsuwaru Mystery.mid


BIN
midis/Kimi no Shiranai Monogatari.mid


BIN
midis/Koibumi.mid


BIN
midis/Koko Kara Hajimaru Monogatari.mid


BIN
midis/Kokoro no Senritsu.mid


BIN
midis/Kono Namida wo Kimi ni Sasagu (2).mid


BIN
midis/Kuchizuke Diamond.mid


BIN
midis/Kuusou Mesorogiwi.mid


BIN
midis/Level 5 - Judgelight.mid


BIN
midis/Life goes on.mid


BIN
midis/Light my Fire.mid


BIN
midis/Little Busters!.mid


BIN
midis/Lumis Eterne.mid


BIN
midis/Madoka Magica - Piano Medley.mid


BIN
midis/Magia.mid


BIN
midis/Main Theme from Non Non Byori.mid


BIN
midis/Masshiro World.mid


BIN
midis/Megumeru - Cuckool mix 2007 -.mid


Some files were not shown because too many files changed in this diff