diff options
| author | pepper <pepper@chimecrisis.com> | 2014-05-15 19:06:59 -0700 |
|---|---|---|
| committer | pepper <pepper@chimecrisis.com> | 2014-05-15 19:06:59 -0700 |
| commit | 93e654a02cef3d5913a2088d4c397418dc0cee2f (patch) | |
| tree | bfad328857e5bcc10906a594470ccc12875d936b | |
doing this
| -rw-r--r-- | .midifile_cli.js.un~ | bin | 0 -> 2354 bytes | |||
| -rw-r--r-- | .midifilecmd.un~ | bin | 0 -> 16051 bytes | |||
| -rw-r--r-- | .test.js.un~ | bin | 0 -> 1811 bytes | |||
| -rw-r--r-- | .test2.js.un~ | bin | 0 -> 1907 bytes | |||
| -rw-r--r-- | LICENSE | 24 | ||||
| -rw-r--r-- | README | 37 | ||||
| -rw-r--r-- | Rakefile | 9 | ||||
| -rw-r--r-- | audio.js | 125 | ||||
| -rw-r--r-- | da.swf | bin | 0 -> 1212 bytes | |||
| -rw-r--r-- | dynamicaudio.as | 80 | ||||
| -rw-r--r-- | gui.min.js | 1 | ||||
| -rw-r--r-- | index.html | 74 | ||||
| -rw-r--r-- | midifile.js | 238 | ||||
| -rw-r--r-- | midifile_cli.js | 313 | ||||
| -rw-r--r-- | midifilecmd | 12 | ||||
| -rw-r--r-- | minute_waltz.mid | bin | 0 -> 16943 bytes | |||
| -rw-r--r-- | rachmaninov3.mid | bin | 0 -> 152806 bytes | |||
| -rw-r--r-- | replayer.js | 171 | ||||
| -rw-r--r-- | sandbox.html | 79 | ||||
| -rw-r--r-- | stream.js | 69 | ||||
| -rw-r--r-- | synth.js | 184 | ||||
| -rw-r--r-- | test.js | 6 | ||||
| -rw-r--r-- | test2.js | 0 |
23 files changed, 1422 insertions, 0 deletions
diff --git a/.midifile_cli.js.un~ b/.midifile_cli.js.un~ Binary files differnew file mode 100644 index 0000000..a289586 --- /dev/null +++ b/.midifile_cli.js.un~ diff --git a/.midifilecmd.un~ b/.midifilecmd.un~ Binary files differnew file mode 100644 index 0000000..86a88df --- /dev/null +++ b/.midifilecmd.un~ diff --git a/.test.js.un~ b/.test.js.un~ Binary files differnew file mode 100644 index 0000000..01a1223 --- /dev/null +++ b/.test.js.un~ diff --git a/.test2.js.un~ b/.test2.js.un~ Binary files differnew file mode 100644 index 0000000..319c9a9 --- /dev/null +++ b/.test2.js.un~ @@ -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. @@ -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' + } + } +} Binary files differdiff --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 Binary files differnew file mode 100644 index 0000000..1d0500e --- /dev/null +++ b/minute_waltz.mid diff --git a/rachmaninov3.mid b/rachmaninov3.mid Binary files differnew file mode 100644 index 0000000..196a535 --- /dev/null +++ b/rachmaninov3.mid 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 + } +} @@ -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 |
