summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpepper <pepper@chimecrisis.com>2014-05-15 19:06:59 -0700
committerpepper <pepper@chimecrisis.com>2014-05-15 19:06:59 -0700
commit93e654a02cef3d5913a2088d4c397418dc0cee2f (patch)
treebfad328857e5bcc10906a594470ccc12875d936b
doing this
-rw-r--r--.midifile_cli.js.un~bin0 -> 2354 bytes
-rw-r--r--.midifilecmd.un~bin0 -> 16051 bytes
-rw-r--r--.test.js.un~bin0 -> 1811 bytes
-rw-r--r--.test2.js.un~bin0 -> 1907 bytes
-rw-r--r--LICENSE24
-rw-r--r--README37
-rw-r--r--Rakefile9
-rw-r--r--audio.js125
-rw-r--r--da.swfbin0 -> 1212 bytes
-rw-r--r--dynamicaudio.as80
-rw-r--r--gui.min.js1
-rw-r--r--index.html74
-rw-r--r--midifile.js238
-rw-r--r--midifile_cli.js313
-rw-r--r--midifilecmd12
-rw-r--r--minute_waltz.midbin0 -> 16943 bytes
-rw-r--r--rachmaninov3.midbin0 -> 152806 bytes
-rw-r--r--replayer.js171
-rw-r--r--sandbox.html79
-rw-r--r--stream.js69
-rw-r--r--synth.js184
-rw-r--r--test.js6
-rw-r--r--test2.js0
23 files changed, 1422 insertions, 0 deletions
diff --git a/.midifile_cli.js.un~ b/.midifile_cli.js.un~
new file mode 100644
index 0000000..a289586
--- /dev/null
+++ b/.midifile_cli.js.un~
Binary files differ
diff --git a/.midifilecmd.un~ b/.midifilecmd.un~
new file mode 100644
index 0000000..86a88df
--- /dev/null
+++ b/.midifilecmd.un~
Binary files differ
diff --git a/.test.js.un~ b/.test.js.un~
new file mode 100644
index 0000000..01a1223
--- /dev/null
+++ b/.test.js.un~
Binary files differ
diff --git a/.test2.js.un~ b/.test2.js.un~
new file mode 100644
index 0000000..319c9a9
--- /dev/null
+++ b/.test2.js.un~
Binary files differ
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..407a442
--- /dev/null
+++ b/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.
diff --git a/README b/README
new file mode 100644
index 0000000..543c840
--- /dev/null
+++ b/README
@@ -0,0 +1,37 @@
+jasmid - A Javascript MIDI file reader and synthesiser
+
+Originally presented at BarCamp London 8, 13-14 November 2010
+
+Instructions:
+Open index.html in browser. Turn up volume. Click on link.
+
+Sound output is via one of the following mechanisms, according to what your
+browser supports:
+* Mozilla Audio Data API <https://wiki.mozilla.org/Audio_Data_API>
+* Web Audio API
+ <https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html>
+* a Flash fallback originally taken from dynamicaudio.js by Ben Firshman
+ <https://github.com/bfirsh/dynamicaudio.js> and hacked around by me.
+
+
+The code:
+stream.js - helper library for reading a string as a stream of typed data
+midifile.js - parses the MIDI file format into a header and a list of tracks,
+ each consisting of a list of event objects
+replayer.js - steps over the data structure generated by midifile.js and calls
+ the appropriate operations on the synthesiser
+synth.js - audio synthesiser; generates waveforms according to tweakable
+ parameters
+audio.js - passes the generated waveform to either the Audio Data API or the
+ Flash fallback widget (da.swf)
+
+
+Limitations:
+* The only event types supported by replayer.js are note on, note off, tempo
+ change and program change
+* There are currently only two instrument presets defined in synth.js - one for
+ strings and a 'piano' one for everything else - and neither of them are
+ particularly good (just a single volume-modulated sine wave).
+
+
+Matt Westcott <matt@west.co.tt> - @gasmanic - http://matt.west.co.tt/
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..daae52c
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,9 @@
+# Replace this with the path to the mxmlc executable in the Flex SDK.
+MXMLC = '/Developer/SDKs/flex_sdk_4.0.0.14159/bin/mxmlc'
+
+task :default => "da.swf"
+
+desc "build the dynamicaudio SWF"
+file "da.swf" => "dynamicaudio.as" do
+ sh %[ #{MXMLC} -use-network=false -o da.swf -file-specs "dynamicaudio.as" ]
+end
diff --git a/audio.js b/audio.js
new file mode 100644
index 0000000..2e21cda
--- /dev/null
+++ b/audio.js
@@ -0,0 +1,125 @@
+var sampleRate = 44100; /* hard-coded in Flash player */
+
+function AudioPlayer(generator, opts) {
+ if (!opts) opts = {};
+ var latency = opts.latency || 1;
+ var checkInterval = latency * 100 /* in ms */
+
+ var audioElement = new Audio();
+ var webkitAudio = window.AudioContext || window.webkitAudioContext;
+ var requestStop = false;
+
+ if (audioElement.mozSetup) {
+ audioElement.mozSetup(2, sampleRate); /* channels, sample rate */
+
+ var buffer = []; /* data generated but not yet written */
+ var minBufferLength = latency * 2 * sampleRate; /* refill buffer when there are only this many elements remaining */
+ var bufferFillLength = Math.floor(latency * sampleRate);
+
+ function checkBuffer() {
+ if (buffer.length) {
+ var written = audioElement.mozWriteAudio(buffer);
+ buffer = buffer.slice(written);
+ }
+ if (buffer.length < minBufferLength && !generator.finished) {
+ buffer = buffer.concat(generator.generate(bufferFillLength));
+ }
+ if (!requestStop && (!generator.finished || buffer.length)) {
+ setTimeout(checkBuffer, checkInterval);
+ }
+ }
+ checkBuffer();
+
+ return {
+ 'type': 'Firefox Audio',
+ 'stop': function() {
+ requestStop = true;
+ }
+ }
+ } else if (webkitAudio) {
+ // Uses Webkit Web Audio API if available
+ var context = new webkitAudio();
+ sampleRate = context.sampleRate;
+
+ var channelCount = 2;
+ var bufferSize = 4096*4; // Higher for less gitches, lower for less latency
+
+ var node = context.createScriptProcessor(bufferSize, 0, channelCount);
+
+ node.onaudioprocess = function(e) { process(e) };
+
+ function process(e) {
+ if (generator.finished) {
+ node.disconnect();
+ return;
+ }
+
+ var dataLeft = e.outputBuffer.getChannelData(0);
+ var dataRight = e.outputBuffer.getChannelData(1);
+
+ var generate = generator.generate(bufferSize);
+
+ for (var i = 0; i < bufferSize; ++i) {
+ dataLeft[i] = generate[i*2];
+ dataRight[i] = generate[i*2+1];
+ }
+ }
+
+ // start
+ node.connect(context.destination);
+
+ return {
+ 'stop': function() {
+ // pause
+ node.disconnect();
+ requestStop = true;
+ },
+ 'type': 'Webkit Audio'
+ }
+
+ } else {
+ // Fall back to creating flash player
+ var c = document.createElement('div');
+ c.innerHTML = '<embed type="application/x-shockwave-flash" id="da-swf" src="da.swf" width="8" height="8" allowScriptAccess="always" style="position: fixed; left:-10px;" />';
+ document.body.appendChild(c);
+ var swf = document.getElementById('da-swf');
+
+ var minBufferDuration = latency * 1000; /* refill buffer when there are only this many ms remaining */
+ var bufferFillLength = latency * sampleRate;
+
+ function write(data) {
+ var out = new Array(data.length);
+ for (var i = data.length-1; i != 0; i--) {
+ out[i] = Math.floor(data[i]*32768);
+ }
+ return swf.write(out.join(' '));
+ }
+
+ function checkBuffer() {
+ if (swf.bufferedDuration() < minBufferDuration) {
+ write(generator.generate(bufferFillLength));
+ };
+ if (!requestStop && !generator.finished) setTimeout(checkBuffer, checkInterval);
+ }
+
+ function checkReady() {
+ if (swf.write) {
+ checkBuffer();
+ } else {
+ setTimeout(checkReady, 10);
+ }
+ }
+ checkReady();
+
+ return {
+ 'stop': function() {
+ swf.stop();
+ requestStop = true;
+ },
+ 'bufferedDuration': function() {
+ return swf.bufferedDuration();
+ },
+ 'type': 'Flash Audio'
+ }
+ }
+}
diff --git a/da.swf b/da.swf
new file mode 100644
index 0000000..e3003d8
--- /dev/null
+++ b/da.swf
Binary files differ
diff --git a/dynamicaudio.as b/dynamicaudio.as
new file mode 100644
index 0000000..45c2ceb
--- /dev/null
+++ b/dynamicaudio.as
@@ -0,0 +1,80 @@
+package {
+ import flash.display.Sprite;
+ import flash.events.SampleDataEvent;
+ import flash.external.ExternalInterface;
+ import flash.media.Sound;
+ import flash.media.SoundChannel;
+
+ public class dynamicaudio extends Sprite {
+ public var bufferSize:Number = 2048; // In samples
+ public var sound:Sound;
+ public var buffer:Array = [];
+ public var channel:SoundChannel;
+ public var writtenSampleCount:Number = 0;
+
+ public function dynamicaudio() {
+ ExternalInterface.addCallback('write', write);
+ ExternalInterface.addCallback('stop', stop);
+ ExternalInterface.addCallback('bufferedDuration', bufferedDuration);
+ this.sound = new Sound();
+ this.sound.addEventListener(
+ SampleDataEvent.SAMPLE_DATA,
+ soundGenerator
+ );
+ this.channel = this.sound.play();
+ }
+
+ // Called from JavaScript to add samples to the buffer
+ // Note we are using a space separated string of samples instead of an
+ // array. Flash's stupid ExternalInterface passes every sample as XML,
+ // which is incredibly expensive to encode/decode
+ public function write(s:String):Number {
+ var multiplier:Number = 1/32768;
+ var alreadyBufferedDuration:Number = (this.writtenSampleCount + this.buffer.length/2) / 44.1;
+ for each (var sample:String in s.split(" ")) {
+ this.buffer.push(Number(sample)*multiplier);
+ }
+ return (this.channel ? alreadyBufferedDuration - this.channel.position : 0);
+ }
+
+ public function bufferedDuration():Number {
+ // duration (in ms) of audio written to Flash so far = (writtenSampleCount * 1000 / sampleRate)
+ // number of ms in Flash's buffer = (writtenSampleCount * 1000 / sampleRate) - this.channel.position
+ // number of ms in our buffer = (this.buffer.length/2 * 1000 / sampleRate)
+ // (/2 because buffer stores stereo data => 2 elements per sample)
+ // for 44100Hz, x * 1000 / sampleRate => x / 44.1
+ return (this.writtenSampleCount + this.buffer.length/2) / 44.1 - this.channel.position;
+ }
+
+ public function stop():void {
+ this.channel.stop();
+ this.buffer = [];
+ this.writtenSampleCount = 0;
+ this.channel = this.sound.play();
+ }
+
+ public function soundGenerator(event:SampleDataEvent):void {
+ var i:int;
+
+ // If we haven't got enough data, write 2048 samples of silence to
+ // both channels, the minimum Flash allows
+ if (this.buffer.length < this.bufferSize*2) {
+ for (i = 0; i < 4096; i++) {
+ event.data.writeFloat(0.0);
+ }
+ this.writtenSampleCount += 2048;
+ return;
+ }
+
+ var count:Number = Math.min(this.buffer.length, 16384);
+
+ for each (var sample:Number in this.buffer.slice(0, count)) {
+ event.data.writeFloat(sample);
+ }
+
+ this.writtenSampleCount += count/2;
+ this.buffer = this.buffer.slice(count, this.buffer.length);
+ }
+ }
+}
+
diff --git a/gui.min.js b/gui.min.js
new file mode 100644
index 0000000..b0b73d3
--- /dev/null
+++ b/gui.min.js
@@ -0,0 +1 @@
+var GUI=function(){var _this=this;var MIN_WIDTH=240;var MAX_WIDTH=500;var head=document.getElementsByTagName("head")[0],style=document.createElement("style"),css="#guidat{position:fixed;top:0;right:0;width:auto;z-index:1001;text-align:right}.guidat{color:#fff;opacity:0.97;text-align:left;float:right;margin-right:20px;margin-bottom:20px;background-color:#fff}.guidat,.guidat input{font:9.5px Lucida Grande,sans-serif}.guidat-controllers{height:300px;overflow-y:auto;overflow-x:hidden;background-color:rgba(0,0,0,0.1)}a.guidat-toggle{text-decoration:none;cursor:pointer;color:#fff;background-color:#222;text-align:center;display:block;padding:5px}a.guidat-toggle:hover{background-color:#000}.guidat-controller{padding:3px;height:25px;clear:left;border-bottom:1px solid #222;background-color:#111}.guidat-controller,.guidat-controller input,.guidat-slider-bg,.guidat-slider-fg{-moz-transition:background-color 0.15s linear;-webkit-transition:background-color 0.15s linear;transition:background-color 0.15s linear}.guidat-controller.boolean:hover,.guidat-controller.function:hover{background-color:#000}.guidat-controller input{float:right;outline:none;border:0;padding:4px;margin-top:2px;background-color:#222}.guidat-controller input:hover{background-color:#444}.guidat-controller input:focus{background-color:#555}.guidat-controller.number{border-left:5px solid #00aeff}.guidat-controller.string{border-left:5px solid #1ed36f}.guidat-controller.string input{border:0;color:#1ed36f;margin-right:2px;width:148px}.guidat-controller.boolean{border-left:5px solid #54396e}.guidat-controller.function{border-left:5px solid #e61d5f}.guidat-controller.number input[type=text]{width:35px;margin-left:5px;margin-right:2px;color:#00aeff}.guidat .guidat-controller.boolean input{margin-top:6px;margin-right:2px;font-size:20px}.guidat-controller:last-child{border-bottom:none;-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.5);-moz-box-shadow:0px 1px 3px rgba(0,0,0,0.5);box-shadow:0px 1px 3px rgba(0,0,0,0.5)}.guidat-propertyname{padding:5px;padding-top:7px;cursor:default;display:inline-block}.guidat-slider-bg:hover,.guidat-slider-bg.active{background-color:#444}.guidat-slider-bg:hover .guidat-slider-fg,.guidat-slider-bg.active .guidat-slider-fg{background-color:#52c8ff}.guidat-slider-bg{background-color:#222;cursor:ew-resize;width:40%;margin-top:2px;float:right;height:21px}.guidat-slider-fg{background-color:#00aeff;height:20px}";style.type="text/css";style.innerHTML=css;head.appendChild(style);var controllers=[];var listening=[];var autoListen=true;var listenInterval;var controllerHeight;var curControllerContainerHeight=0;var _this=this;var open=false;var width=280;var explicitOpenHeight=false;var openHeight;var name;var resizeTo=0;var resizeTimeout;this.domElement=document.createElement("div");this.domElement.setAttribute("class","guidat");this.domElement.style.width=width+"px";var controllerContainer=document.createElement("div");controllerContainer.setAttribute("class","guidat-controllers");controllerContainer.addEventListener("DOMMouseScroll",function(e){var scrollAmount=this.scrollTop;if(e.wheelDelta){scrollAmount+=e.wheelDelta}else{if(e.detail){scrollAmount+=e.detail}}if(e.preventDefault){e.preventDefault()}e.returnValue=false;controllerContainer.scrollTop=scrollAmount},false);controllerContainer.style.height="0px";var toggleButton=document.createElement("a");toggleButton.setAttribute("class","guidat-toggle");toggleButton.setAttribute("href","#");toggleButton.innerHTML="Show Controls";var toggleDragged=false;var dragDisplacementY=0;var togglePressed=false;var my,pmy,mx,pmx;var resize=function(e){pmy=my;pmx=mx;my=e.pageY;mx=e.pageX;var dmy=my-pmy;if(!open){if(dmy>0){open=true;curControllerContainerHeight=openHeight=1;toggleButton.innerHTML=name||"Hide Controls"}else{return}}var dmx=pmx-mx;if(dmy>0&&curControllerContainerHeight>controllerHeight){var d=GUI.map(curControllerContainerHeight,controllerHeight,controllerHeight+100,1,0);dmy*=d}toggleDragged=true;dragDisplacementY+=dmy;dragDisplacementX+=dmx;openHeight+=dmy;width+=dmx;curControllerContainerHeight+=dmy;controllerContainer.style.height=openHeight+"px";width=GUI.constrain(width,MIN_WIDTH,MAX_WIDTH);_this.domElement.style.width=width+"px";checkForOverflow()};toggleButton.addEventListener("mousedown",function(e){pmy=my=e.pageY;pmx=mx=e.pageX;togglePressed=true;e.preventDefault();dragDisplacementY=0;dragDisplacementX=0;document.addEventListener("mousemove",resize,false);return false},false);toggleButton.addEventListener("click",function(e){e.preventDefault();return false},false);document.addEventListener("mouseup",function(e){if(togglePressed&&!toggleDragged){_this.toggle();_this.domElement.style.width=(width+1)+"px";setTimeout(function(){_this.domElement.style.width=width+"px"},1)}if(togglePressed&&toggleDragged){if(dragDisplacementX==0){_this.domElement.style.width=(width+1)+"px";setTimeout(function(){_this.domElement.style.width=width+"px"},1)}if(openHeight>controllerHeight){clearTimeout(resizeTimeout);openHeight=resizeTo=controllerHeight;beginResize()}else{if(controllerContainer.children.length>=1){var singleControllerHeight=controllerContainer.children[0].offsetHeight;clearTimeout(resizeTimeout);var target=Math.round(curControllerContainerHeight/singleControllerHeight)*singleControllerHeight-1;resizeTo=target;if(resizeTo<=0){_this.hide();openHeight=singleControllerHeight*2}else{openHeight=resizeTo;beginResize()}}}}document.removeEventListener("mousemove",resize,false);e.preventDefault();toggleDragged=false;togglePressed=false;return false},false);this.domElement.appendChild(controllerContainer);this.domElement.appendChild(toggleButton);if(GUI.autoPlace){if(GUI.autoPlaceContainer==null){GUI.autoPlaceContainer=document.createElement("div");GUI.autoPlaceContainer.setAttribute("id","guidat");document.body.appendChild(GUI.autoPlaceContainer)}GUI.autoPlaceContainer.appendChild(this.domElement)}this.autoListenIntervalTime=1000/60;var createListenInterval=function(){listenInterval=setInterval(function(){_this.listen()},this.autoListenIntervalTime)};this.__defineSetter__("autoListen",function(v){autoListen=v;if(!autoListen){clearInterval(listenInterval)}else{if(listening.length>0){createListenInterval()}}});this.__defineGetter__("autoListen",function(v){return autoListen});this.listenTo=function(controller){if(listening.length==0){createListenInterval()}listening.push(controller)};this.unlistenTo=function(controller){for(var i=0;i<listening.length;i++){if(listening[i]==controller){listening.splice(i,1)}}if(listening.length<=0){clearInterval(listenInterval)}};this.listen=function(whoToListenTo){var arr=whoToListenTo||listening;for(var i in arr){arr[i].updateDisplay()}};this.listenAll=function(){this.listen(controllers)};this.autoListen=true;var alreadyControlled=function(object,propertyName){for(var i in controllers){if(controllers[i].object==object&&controllers[i].propertyName==propertyName){return true}}return false};var construct=function(constructor,args){function F(){return constructor.apply(this,args)}F.prototype=constructor.prototype;return new F()};this.add=function(){var object=arguments[0];var propertyName=arguments[1];if(alreadyControlled(object,propertyName)){}var value=object[propertyName];if(value==undefined){GUI.error(object+' either has no property "'+propertyName+'", or the property is inaccessible.');return}var type=typeof value;var handler=handlerTypes[type];if(handler==undefined){GUI.error('Cannot create controller for data type "'+type+'"');return}var args=[this];for(var j=0;j<arguments.length;j++){args.push(arguments[j])}var controllerObject=construct(handler,args);if(!controllerObject){GUI.error('Error creating controller for "'+propertyName+'".');return}controllerContainer.appendChild(controllerObject.domElement);controllers.push(controllerObject);GUI.allControllers.push(controllerObject);if(type!="function"&&GUI.saveIndex<GUI.savedValues.length){controllerObject.setValue(GUI.savedValues[GUI.saveIndex]);GUI.saveIndex++}checkForOverflow();if(!explicitOpenHeight){openHeight=controllerHeight}return controllerObject};var checkForOverflow=function(){controllerHeight=0;for(var i in controllers){controllerHeight+=controllers[i].domElement.offsetHeight}if(controllerHeight-1>openHeight){controllerContainer.style.overflowY="auto"}else{controllerContainer.style.overflowY="hidden"}};var handlerTypes={number:GUI.NumberController,string:GUI.StringController,"boolean":GUI.BooleanController,"function":GUI.FunctionController};var alreadyControlled=function(object,propertyName){for(var i in controllers){if(controllers[i].object==object&&controllers[i].propertyName==propertyName){return true}}return false};var construct=function(constructor,args){function F(){return constructor.apply(this,args)}F.prototype=constructor.prototype;return new F()};this.reset=function(){};this.toggle=function(){open?this.hide():this.show()};this.show=function(){toggleButton.innerHTML=name||"Hide Controls";resizeTo=openHeight;clearTimeout(resizeTimeout);beginResize();open=true};this.hide=function(){toggleButton.innerHTML=name||"Show Controls";resizeTo=0;clearTimeout(resizeTimeout);beginResize();open=false};this.name=function(n){name=n;toggleButton.innerHTML=n};this.appearanceVars=function(){return[open,width,openHeight,controllerContainer.scrollTop]};var beginResize=function(){curControllerContainerHeight+=(resizeTo-curControllerContainerHeight)*0.6;if(Math.abs(curControllerContainerHeight-resizeTo)<1){curControllerContainerHeight=resizeTo}else{resizeTimeout=setTimeout(beginResize,1000/30)}controllerContainer.style.height=Math.round(curControllerContainerHeight)+"px";checkForOverflow()};if(GUI.guiIndex<GUI.savedAppearanceVars.length){width=parseInt(GUI.savedAppearanceVars[GUI.guiIndex][1]);_this.domElement.style.width=width+"px";openHeight=parseInt(GUI.savedAppearanceVars[GUI.guiIndex][2]);explicitOpenHeight=true;if(eval(GUI.savedAppearanceVars[GUI.guiIndex][0])==true){curControllerContainerHeight=openHeight;var t=GUI.savedAppearanceVars[GUI.guiIndex][3];setTimeout(function(){controllerContainer.scrollTop=t},0);if(GUI.scrollTop>-1){document.body.scrollTop=GUI.scrollTop}resizeTo=openHeight;this.show()}GUI.guiIndex++}GUI.allGuis.push(this)};GUI.autoPlace=true;GUI.autoPlaceContainer=null;GUI.allControllers=[];GUI.allGuis=[];GUI.saveURL=function(){title=window.location;url=GUI.replaceGetVar("saveString",GUI.getSaveString());window.location=url};GUI.scrollTop=-1;GUI.load=function(c){var d=c.split(",");var a=parseInt(d[0]);GUI.scrollTop=parseInt(d[1]);for(var b=0;b<a;b++){var e=d.splice(2,4);GUI.savedAppearanceVars.push(e)}GUI.savedValues=d.splice(2,d.length)};GUI.savedValues=[];GUI.savedAppearanceVars=[];GUI.getSaveString=function(){var d=[];d.push(GUI.allGuis.length);d.push(document.body.scrollTop);for(var c in GUI.allGuis){var e=GUI.allGuis[c].appearanceVars();for(var b=0;b<e.length;b++){d.push(e[b])}}for(var c in GUI.allControllers){if(GUI.allControllers[c].type=="function"){continue}var a=GUI.allControllers[c].getValue();if(GUI.allControllers[c].type=="number"){a=GUI.roundToDecimal(a,4)}d.push(a)}return d.join(",")};GUI.getVarFromURL=function(a){var e=[],d;var b=window.location.href.slice(window.location.href.indexOf("?")+1).split("&");for(var c=0;c<b.length;c++){d=b[c].split("=");if(d==undefined){continue}if(d[0]==a){return d[1]}}return null};GUI.replaceGetVar=function(g,f){var d=[],c;var e=window.location.href;var a=window.location.href.slice(window.location.href.indexOf("?")+1).split("&");for(var b=0;b<a.length;b++){c=a[b].split("=");if(c==undefined){continue}if(c[0]==g){return e.replace(c[1],f)}}if(window.location.href.indexOf("?")!=-1){return e+"&"+g+"="+f}return e+"?"+g+"="+f};GUI.saveIndex=0;GUI.guiIndex=0;GUI.showSaveString=function(){alert(GUI.getSaveString())};GUI.makeUnselectable=function(a){a.onselectstart=function(){return false};a.style.MozUserSelect="none";a.style.KhtmlUserSelect="none";a.unselectable="on"};GUI.makeSelectable=function(a){a.onselectstart=function(){};a.style.MozUserSelect="auto";a.style.KhtmlUserSelect="auto";a.unselectable="off"};GUI.map=function(a,d,b,e,c){var a=e+(c-e)*((a-d)/(b-d));return a};GUI.constrain=function(a,c,b){if(a<c){a=c}else{if(a>b){a=b}}return a};GUI.error=function(a){if(typeof console.error=="function"){console.error("[GUI ERROR] "+a)}};GUI.roundToDecimal=function(c,a){var b=Math.pow(10,a);return Math.round(c*b)/b};GUI.extendController=function(a){a.prototype=new GUI.Controller();a.prototype.constructor=a};if(GUI.getVarFromURL("saveString")!=null){GUI.load(GUI.getVarFromURL("saveString"))}GUI.Slider=function(a,d,i,b,h){var d=d;var i=i;var b=b;var g=false;var e=this;var j,k;this.domElement=document.createElement("div");this.domElement.setAttribute("class","guidat-slider-bg");this.fg=document.createElement("div");this.fg.setAttribute("class","guidat-slider-fg");this.domElement.appendChild(this.fg);var f=function(l){var m=curtop=0;if(l.offsetParent){do{m+=l.offsetLeft;curtop+=l.offsetTop}while(l=l.offsetParent);return[m,curtop]}};this.__defineSetter__("value",function(m){var l=GUI.map(m,d,i,0,100);this.fg.style.width=l+"%"});var c=function(l){if(!g){return}var n=f(e.domElement);var m=GUI.map(l.pageX,n[0],n[0]+e.domElement.offsetWidth,d,i);m=Math.round(m/b)*b;a.setValue(m)};this.domElement.addEventListener("mousedown",function(l){g=true;j=k=l.pageX;e.domElement.setAttribute("class","guidat-slider-bg active");e.fg.setAttribute("class","guidat-slider-fg active");c(l)},false);document.addEventListener("mouseup",function(l){e.domElement.setAttribute("class","guidat-slider-bg");e.fg.setAttribute("class","guidat-slider-fg");g=false},false);document.addEventListener("mousemove",c,false);this.value=h};GUI.Controller=function(){this.parent=arguments[0];this.object=arguments[1];this.propertyName=arguments[2];if(arguments.length>0){this.initialValue=this.propertyName[this.object]}this.domElement=document.createElement("div");this.domElement.setAttribute("class","guidat-controller "+this.type);this.propertyNameElement=document.createElement("span");this.propertyNameElement.setAttribute("class","guidat-propertyname");this.name(this.propertyName);this.domElement.appendChild(this.propertyNameElement);GUI.makeUnselectable(this.domElement)};GUI.Controller.prototype.changeFunction=null;GUI.Controller.prototype.name=function(a){this.propertyNameElement.innerHTML=a;return this};GUI.Controller.prototype.reset=function(){this.setValue(this.initialValue);return this};GUI.Controller.prototype.listen=function(){this.parent.listenTo(this);return this};GUI.Controller.prototype.unlisten=function(){this.parent.unlistenTo(this);return this};GUI.Controller.prototype.setValue=function(a){this.object[this.propertyName]=a;if(this.changeFunction!=null){this.changeFunction.call(this,a)}this.updateDisplay();return this};GUI.Controller.prototype.getValue=function(){return this.object[this.propertyName]};GUI.Controller.prototype.updateDisplay=function(){};GUI.Controller.prototype.onChange=function(a){this.changeFunction=a;return this};GUI.StringController=function(){this.type="string";var c=this;GUI.Controller.apply(this,arguments);var b=document.createElement("input");var a=this.getValue();b.setAttribute("value",a);b.setAttribute("spellcheck","false");this.domElement.addEventListener("mouseup",function(){b.focus();b.select()},false);b.addEventListener("keyup",function(){c.setValue(b.value)},false);this.updateDisplay=function(){b.value=c.getValue()};this.domElement.appendChild(b)};GUI.extendController(GUI.StringController);GUI.NumberController=function(){this.type="number";GUI.Controller.apply(this,arguments);var f=this;var a=false;var g=false;var i=py=0;var e=arguments[3];var j=arguments[4];var d=arguments[5];if(!d){if(e!=undefined&&j!=undefined){d=(j-e)*0.01}else{d=1}}var c=document.createElement("input");c.setAttribute("id",this.propertyName);c.setAttribute("type","text");c.setAttribute("value",this.getValue());if(d){c.setAttribute("step",d)}this.domElement.appendChild(c);var b;if(e!=undefined&&j!=undefined){b=new GUI.Slider(this,e,j,d,this.getValue());this.domElement.appendChild(b.domElement)}c.addEventListener("blur",function(k){var l=parseFloat(this.value);if(!isNaN(l)){f.updateDisplay()}else{this.value=f.getValue()}},false);c.addEventListener("mousewheel",function(k){k.preventDefault();f.setValue(f.getValue()+Math.abs(k.wheelDeltaY)/k.wheelDeltaY*d);return false},false);c.addEventListener("mousedown",function(k){py=i=k.pageY;g=true;document.addEventListener("mousemove",h,false)},false);c.addEventListener("keydown",function(l){switch(l.keyCode){case 38:var k=f.getValue()+d;f.setValue(k);break;case 40:var k=f.getValue()-d;f.setValue(k);break}},false);document.addEventListener("mouseup",function(k){document.removeEventListener("mousemove",h,false);GUI.makeSelectable(f.parent.domElement);GUI.makeSelectable(c);if(g&&!a){c.focus();c.select()}a=false;g=false},false);var h=function(m){a=true;m.preventDefault();GUI.makeUnselectable(f.parent.domElement);GUI.makeUnselectable(c);py=i;i=m.pageY;var k=py-i;var l=f.getValue()+k*d;f.setValue(l);return false};this.setValue=function(k){k=parseFloat(k);if(e!=undefined&&k<=e){k=e}else{if(j!=undefined&&k>=j){k=j}}return GUI.Controller.prototype.setValue.call(this,k)};this.updateDisplay=function(){c.value=GUI.roundToDecimal(f.getValue(),4);if(b){b.value=f.getValue()}}};GUI.extendController(GUI.NumberController);GUI.FunctionController=function(){this.type="function";var a=this;GUI.Controller.apply(this,arguments);this.domElement.addEventListener("click",function(){a.object[a.propertyName].call(a.object)},false);this.domElement.style.cursor="pointer";this.propertyNameElement.style.cursor="pointer"};GUI.extendController(GUI.FunctionController);GUI.BooleanController=function(){this.type="boolean";GUI.Controller.apply(this,arguments);var _this=this;var input=document.createElement("input");input.setAttribute("type","checkbox");this.domElement.addEventListener("click",function(e){input.checked=!input.checked;e.preventDefault();_this.setValue(input.checked)},false);input.addEventListener("mouseup",function(e){input.checked=!input.checked},false);this.domElement.style.cursor="pointer";this.propertyNameElement.style.cursor="pointer";this.domElement.appendChild(input);this.updateDisplay=function(){input.checked=_this.getValue()};this.setValue=function(val){if(typeof val!="boolean"){try{val=eval(val)}catch(e){}}return GUI.Controller.prototype.setValue.call(this,val)}};GUI.extendController(GUI.BooleanController); \ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..c14deb6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <script src="stream.js"></script>
+ <script src="midifile.js"></script>
+ <script src="replayer.js"></script>
+ <script src="synth.js"></script>
+ <script src="audio.js"></script>
+ <script>
+ function loadRemote(path, callback) {
+ var fetch = new XMLHttpRequest();
+ fetch.open('GET', path);
+ fetch.overrideMimeType("text/plain; charset=x-user-defined");
+ fetch.onreadystatechange = function() {
+ if(this.readyState == 4 && this.status == 200) {
+ /* munge response into a binary string */
+ 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);
+ }
+ callback(ff.join(""));
+ }
+ }
+ fetch.send();
+ }
+
+ function play(file) {
+ loadRemote(file, function(data) {
+ midiFile = MidiFile(data);
+ synth = Synth(44100);
+ replayer = Replayer(midiFile, synth);
+ audio = AudioPlayer(replayer);
+ })
+ }
+
+ if(FileReader){
+ function cancelEvent(e){
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ document.addEventListener('dragenter', cancelEvent, false);
+ document.addEventListener('dragover', cancelEvent, false);
+ document.addEventListener('drop', function(e){
+ cancelEvent(e);
+ for(var i=0;i<e.dataTransfer.files.length;++i){
+ var
+ file = e.dataTransfer.files[i]
+ ;
+ if(file.type != 'audio/midi'){
+ continue;
+ }
+ var
+ reader = new FileReader()
+ ;
+ reader.onload = function(e){
+ midiFile = MidiFile(e.target.result);
+ synth = Synth(44100);
+ replayer = Replayer(midiFile, synth);
+ audio = AudioPlayer(replayer);
+ };
+ reader.readAsBinaryString(file);
+ }
+ }, false);
+ }
+ </script>
+ </head>
+ <body>
+ <a href="javascript:void(play('minute_waltz.mid'))">Chopin - Waltz Op.61 (Minute Waltz)</a> |
+ <a href="javascript:void(play('rachmaninov3.mid'))">Rachmaninov - Piano Concerto No.3 (First movement)</a>
+ </body>
+</html>
diff --git a/midifile.js b/midifile.js
new file mode 100644
index 0000000..8062f15
--- /dev/null
+++ b/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
+ }
+}
diff --git a/midifile_cli.js b/midifile_cli.js
new file mode 100644
index 0000000..ee4a39e
--- /dev/null
+++ b/midifile_cli.js
@@ -0,0 +1,313 @@
+/* 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
+ }
+}
+/*
+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
+ }
+}
+
+
+module.exports = {
+ processMidiData : MidiFile
+
+}
diff --git a/midifilecmd b/midifilecmd
new file mode 100644
index 0000000..e3ee9f7
--- /dev/null
+++ b/midifilecmd
@@ -0,0 +1,12 @@
+#!/usr/bin/env node
+var fs = require('fs');
+var midiF = require('./midifile_cli');
+var filepath = "/tmp/example.mid";
+fs.readFile(filepath, 'utf8', function(err, data) {
+ if(err) {
+ console.error("Could not open file: %s", err);
+ return;
+ }
+ console.log(data);
+// console.log(midiF.processMidiData(data));
+});
diff --git a/minute_waltz.mid b/minute_waltz.mid
new file mode 100644
index 0000000..1d0500e
--- /dev/null
+++ b/minute_waltz.mid
Binary files differ
diff --git a/rachmaninov3.mid b/rachmaninov3.mid
new file mode 100644
index 0000000..196a535
--- /dev/null
+++ b/rachmaninov3.mid
Binary files differ
diff --git a/replayer.js b/replayer.js
new file mode 100644
index 0000000..d1c8b4b
--- /dev/null
+++ b/replayer.js
@@ -0,0 +1,171 @@
+function Replayer(midiFile, synth) {
+ var trackStates = [];
+ var beatsPerMinute = 120;
+ var ticksPerBeat = midiFile.header.ticksPerBeat;
+ var channelCount = 16;
+
+ for (var i = 0; i < midiFile.tracks.length; i++) {
+ trackStates[i] = {
+ 'nextEventIndex': 0,
+ 'ticksToNextEvent': (
+ midiFile.tracks[i].length ?
+ midiFile.tracks[i][0].deltaTime :
+ null
+ )
+ };
+ }
+
+ function Channel() {
+
+ var generatorsByNote = {};
+ var currentProgram = PianoProgram;
+
+ function noteOn(note, velocity) {
+ if (generatorsByNote[note] && !generatorsByNote[note].released) {
+ /* playing same note before releasing the last one. BOO */
+ generatorsByNote[note].noteOff(); /* TODO: check whether we ought to be passing a velocity in */
+ }
+ generator = currentProgram.createNote(note, velocity);
+ synth.addGenerator(generator);
+ generatorsByNote[note] = generator;
+ }
+ function noteOff(note, velocity) {
+ if (generatorsByNote[note] && !generatorsByNote[note].released) {
+ generatorsByNote[note].noteOff(velocity);
+ }
+ }
+ function setProgram(programNumber) {
+ currentProgram = PROGRAMS[programNumber] || PianoProgram;
+ }
+
+ return {
+ 'noteOn': noteOn,
+ 'noteOff': noteOff,
+ 'setProgram': setProgram
+ }
+ }
+
+ var channels = [];
+ for (var i = 0; i < channelCount; i++) {
+ channels[i] = Channel();
+ }
+
+ 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
+ }
+ }
+ nextEventInfo = {
+ 'ticksToEvent': ticksToNextEvent,
+ 'event': nextEvent,
+ 'track': nextEventTrack
+ }
+ var beatsToNextEvent = ticksToNextEvent / ticksPerBeat;
+ var secondsToNextEvent = beatsToNextEvent / (beatsPerMinute / 60);
+ samplesToNextEvent += secondsToNextEvent * synth.sampleRate;
+ } else {
+ nextEventInfo = null;
+ samplesToNextEvent = null;
+ self.finished = true;
+ }
+ }
+
+ getNextEvent();
+
+ function generate(samples) {
+ var data = new Array(samples*2);
+ var samplesRemaining = samples;
+ var dataOffset = 0;
+
+ while (true) {
+ if (samplesToNextEvent != null && samplesToNextEvent <= samplesRemaining) {
+ /* generate samplesToNextEvent samples, process event and repeat */
+ var samplesToGenerate = Math.ceil(samplesToNextEvent);
+ if (samplesToGenerate > 0) {
+ synth.generateIntoBuffer(samplesToGenerate, data, dataOffset);
+ dataOffset += samplesToGenerate * 2;
+ samplesRemaining -= samplesToGenerate;
+ samplesToNextEvent -= samplesToGenerate;
+ }
+
+ handleEvent();
+ getNextEvent();
+ } else {
+ /* generate samples to end of buffer */
+ if (samplesRemaining > 0) {
+ synth.generateIntoBuffer(samplesRemaining, data, dataOffset);
+ samplesToNextEvent -= samplesRemaining;
+ }
+ break;
+ }
+ }
+ return data;
+ }
+
+ function handleEvent() {
+ var event = nextEventInfo.event;
+ switch (event.type) {
+ case 'meta':
+ switch (event.subtype) {
+ case 'setTempo':
+ beatsPerMinute = 60000000 / event.microsecondsPerBeat
+ }
+ break;
+ case 'channel':
+ switch (event.subtype) {
+ case 'noteOn':
+ channels[event.channel].noteOn(event.noteNumber, event.velocity);
+ break;
+ case 'noteOff':
+ channels[event.channel].noteOff(event.noteNumber, event.velocity);
+ break;
+ case 'programChange':
+ //console.log('program change to ' + event.programNumber);
+ channels[event.channel].setProgram(event.programNumber);
+ break;
+ }
+ break;
+ }
+ }
+
+ function replay(audio) {
+ console.log('replay');
+ audio.write(generate(44100));
+ setTimeout(function() {replay(audio)}, 10);
+ }
+
+ var self = {
+ 'replay': replay,
+ 'generate': generate,
+ 'finished': false
+ }
+ return self;
+}
diff --git a/sandbox.html b/sandbox.html
new file mode 100644
index 0000000..6fcaa61
--- /dev/null
+++ b/sandbox.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <script src="synth.js"></script>
+ <script src="audio.js"></script>
+ <script src="gui.min.js"></script>
+ <script>
+ function makeGui() {
+ var gui = new GUI();
+ gui.add(TestProgram, 'waveform', 0, 1);
+ gui.add(TestProgram, 'attackAmplitude', 0, 1);
+ gui.add(TestProgram, 'sustainAmplitude', 0, 1);
+ gui.add(TestProgram, 'attackTime', 0, 1);
+ gui.add(TestProgram, 'decayTime', 0, 1);
+ gui.add(TestProgram, 'releaseTime', 0, 1);
+ gui.add(TestProgram, 'phase', 0, 1);
+ }
+
+ TestProgram = {
+ 'waveform': 0,
+ 'attackAmplitude': 0.2,
+ 'sustainAmplitude': 0.1,
+ 'attackTime': 0.02,
+ 'decayTime': 0.3,
+ 'releaseTime': 0.02,
+ 'phase': 0.5,
+ 'createNote': function(note, velocity) {
+ var frequency = midiToFrequency(note);
+ return ADSRGenerator(
+ (this.waveform ? SquareGenerator(frequency, this.phase) : SineGenerator(frequency)),
+ this.attackAmplitude * (velocity / 128), this.sustainAmplitude * (velocity / 128),
+ this.attackTime, this.decayTime, this.releaseTime
+ );
+ }
+ }
+
+ function go(file) {
+ synth = Synth(44100);
+ var samplesToNextNote = 0;
+ var currentGenerator = null;
+ var noteNumber = 0;
+ replayer = {
+ 'generate': function(samples) {
+ var data = new Array(samples*2);
+ var samplesRemaining = samples;
+ var dataOffset = 0;
+
+ while (samplesRemaining) {
+ if (samplesToNextNote < samplesRemaining) {
+ if (samplesToNextNote > 0) {
+ synth.generateIntoBuffer(samplesToNextNote, data, dataOffset);
+ dataOffset += samplesToNextNote * 2;
+ }
+ if (currentGenerator) {
+ currentGenerator.noteOff();
+ }
+ currentGenerator = TestProgram.createNote(50 + noteNumber, 127);
+ noteNumber = (noteNumber + 1) % 12;
+ synth.addGenerator(currentGenerator);
+ samplesRemaining -= samplesToNextNote;
+ samplesToNextNote = 11025;
+ } else {
+ /* generate samplesRemaining */
+ synth.generateIntoBuffer(samplesRemaining, data, dataOffset);
+ samplesToNextNote -= samplesRemaining;
+ samplesRemaining = 0;
+ }
+ }
+ return data;
+ }
+ }
+ audio = AudioPlayer(replayer, {'latency': 0.5});
+ }
+ </script>
+ </head>
+ <body onload="makeGui()">
+ <a href="javascript:void(go())">go</a>
+ </body>
+</html>
diff --git a/stream.js b/stream.js
new file mode 100644
index 0000000..d7af2ba
--- /dev/null
+++ b/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
+ }
+}
diff --git a/synth.js b/synth.js
new file mode 100644
index 0000000..ac05cb3
--- /dev/null
+++ b/synth.js
@@ -0,0 +1,184 @@
+function SineGenerator(freq) {
+ var self = {'alive': true};
+ var period = sampleRate / freq;
+ var t = 0;
+
+ self.generate = function(buf, offset, count) {
+ for (; count; count--) {
+ var phase = t / period;
+ var result = Math.sin(phase * 2 * Math.PI);
+ buf[offset++] += result;
+ buf[offset++] += result;
+ t++;
+ }
+ }
+
+ return self;
+}
+
+function SquareGenerator(freq, phase) {
+ var self = {'alive': true};
+ var period = sampleRate / freq;
+ var t = 0;
+
+ self.generate = function(buf, offset, count) {
+ for (; count; count--) {
+ var result = ( (t / period) % 1 > phase ? 1 : -1);
+ buf[offset++] += result;
+ buf[offset++] += result;
+ t++;
+ }
+ }
+
+ return self;
+}
+
+function ADSRGenerator(child, attackAmplitude, sustainAmplitude, attackTimeS, decayTimeS, releaseTimeS) {
+ var self = {'alive': true}
+ var attackTime = sampleRate * attackTimeS;
+ var decayTime = sampleRate * (attackTimeS + decayTimeS);
+ var decayRate = (attackAmplitude - sustainAmplitude) / (decayTime - attackTime);
+ var releaseTime = null; /* not known yet */
+ var endTime = null; /* not known yet */
+ var releaseRate = sustainAmplitude / (sampleRate * releaseTimeS);
+ var t = 0;
+
+ self.noteOff = function() {
+ if (self.released) return;
+ releaseTime = t;
+ self.released = true;
+ endTime = releaseTime + sampleRate * releaseTimeS;
+ }
+
+ self.generate = function(buf, offset, count) {
+ if (!self.alive) return;
+ var input = new Array(count * 2);
+ for (var i = 0; i < count*2; i++) {
+ input[i] = 0;
+ }
+ child.generate(input, 0, count);
+
+ childOffset = 0;
+ while(count) {
+ if (releaseTime != null) {
+ if (t < endTime) {
+ /* release */
+ while(count && t < endTime) {
+ var ampl = sustainAmplitude - releaseRate * (t - releaseTime);
+ buf[offset++] += input[childOffset++] * ampl;
+ buf[offset++] += input[childOffset++] * ampl;
+ t++;
+ count--;
+ }
+ } else {
+ /* dead */
+ self.alive = false;
+ return;
+ }
+ } else if (t < attackTime) {
+ /* attack */
+ while(count && t < attackTime) {
+ var ampl = attackAmplitude * t / attackTime;
+ buf[offset++] += input[childOffset++] * ampl;
+ buf[offset++] += input[childOffset++] * ampl;
+ t++;
+ count--;
+ }
+ } else if (t < decayTime) {
+ /* decay */
+ while(count && t < decayTime) {
+ var ampl = attackAmplitude - decayRate * (t - attackTime);
+ buf[offset++] += input[childOffset++] * ampl;
+ buf[offset++] += input[childOffset++] * ampl;
+ t++;
+ count--;
+ }
+ } else {
+ /* sustain */
+ while(count) {
+ buf[offset++] += input[childOffset++] * sustainAmplitude;
+ buf[offset++] += input[childOffset++] * sustainAmplitude;
+ t++;
+ count--;
+ }
+ }
+ }
+ }
+
+ return self;
+}
+
+function midiToFrequency(note) {
+ return 440 * Math.pow(2, (note-69)/12);
+}
+
+PianoProgram = {
+ 'attackAmplitude': 0.2,
+ 'sustainAmplitude': 0.1,
+ 'attackTime': 0.02,
+ 'decayTime': 0.3,
+ 'releaseTime': 0.02,
+ 'createNote': function(note, velocity) {
+ var frequency = midiToFrequency(note);
+ return ADSRGenerator(
+ SineGenerator(frequency),
+ this.attackAmplitude * (velocity / 128), this.sustainAmplitude * (velocity / 128),
+ this.attackTime, this.decayTime, this.releaseTime
+ );
+ }
+}
+
+StringProgram = {
+ 'createNote': function(note, velocity) {
+ var frequency = midiToFrequency(note);
+ return ADSRGenerator(
+ SineGenerator(frequency),
+ 0.5 * (velocity / 128), 0.2 * (velocity / 128),
+ 0.4, 0.8, 0.4
+ );
+ }
+}
+
+PROGRAMS = {
+ 41: StringProgram,
+ 42: StringProgram,
+ 43: StringProgram,
+ 44: StringProgram,
+ 45: StringProgram,
+ 46: StringProgram,
+ 47: StringProgram,
+ 49: StringProgram,
+ 50: StringProgram
+};
+
+function Synth(sampleRate) {
+
+ var generators = [];
+
+ function addGenerator(generator) {
+ generators.push(generator);
+ }
+
+ function generate(samples) {
+ var data = new Array(samples*2);
+ generateIntoBuffer(samples, data, 0);
+ return data;
+ }
+
+ function generateIntoBuffer(samplesToGenerate, buffer, offset) {
+ for (var i = offset; i < offset + samplesToGenerate * 2; i++) {
+ buffer[i] = 0;
+ }
+ for (var i = generators.length - 1; i >= 0; i--) {
+ generators[i].generate(buffer, offset, samplesToGenerate);
+ if (!generators[i].alive) generators.splice(i, 1);
+ }
+ }
+
+ return {
+ 'sampleRate': sampleRate,
+ 'addGenerator': addGenerator,
+ 'generate': generate,
+ 'generateIntoBuffer': generateIntoBuffer
+ }
+}
diff --git a/test.js b/test.js
new file mode 100644
index 0000000..8730322
--- /dev/null
+++ b/test.js
@@ -0,0 +1,6 @@
+function testme(){
+ console.log("hello");
+}
+module.exports = {
+ bj: testme
+};
diff --git a/test2.js b/test2.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test2.js