From 93e654a02cef3d5913a2088d4c397418dc0cee2f Mon Sep 17 00:00:00 2001 From: pepper Date: Thu, 15 May 2014 19:06:59 -0700 Subject: doing this --- .midifile_cli.js.un~ | Bin 0 -> 2354 bytes .midifilecmd.un~ | Bin 0 -> 16051 bytes .test.js.un~ | Bin 0 -> 1811 bytes .test2.js.un~ | Bin 0 -> 1907 bytes LICENSE | 24 ++++ README | 37 ++++++ Rakefile | 9 ++ audio.js | 125 ++++++++++++++++++++ da.swf | Bin 0 -> 1212 bytes dynamicaudio.as | 80 +++++++++++++ gui.min.js | 1 + index.html | 74 ++++++++++++ midifile.js | 238 +++++++++++++++++++++++++++++++++++++++ midifile_cli.js | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++ midifilecmd | 12 ++ minute_waltz.mid | Bin 0 -> 16943 bytes rachmaninov3.mid | Bin 0 -> 152806 bytes replayer.js | 171 ++++++++++++++++++++++++++++ sandbox.html | 79 +++++++++++++ stream.js | 69 ++++++++++++ synth.js | 184 ++++++++++++++++++++++++++++++ test.js | 6 + test2.js | 0 23 files changed, 1422 insertions(+) create mode 100644 .midifile_cli.js.un~ create mode 100644 .midifilecmd.un~ create mode 100644 .test.js.un~ create mode 100644 .test2.js.un~ create mode 100644 LICENSE create mode 100644 README create mode 100644 Rakefile create mode 100644 audio.js create mode 100644 da.swf create mode 100644 dynamicaudio.as create mode 100644 gui.min.js create mode 100644 index.html create mode 100644 midifile.js create mode 100644 midifile_cli.js create mode 100644 midifilecmd create mode 100644 minute_waltz.mid create mode 100644 rachmaninov3.mid create mode 100644 replayer.js create mode 100644 sandbox.html create mode 100644 stream.js create mode 100644 synth.js create mode 100644 test.js create mode 100644 test2.js diff --git a/.midifile_cli.js.un~ b/.midifile_cli.js.un~ new file mode 100644 index 0000000..a289586 Binary files /dev/null and b/.midifile_cli.js.un~ differ diff --git a/.midifilecmd.un~ b/.midifilecmd.un~ new file mode 100644 index 0000000..86a88df Binary files /dev/null and b/.midifilecmd.un~ differ diff --git a/.test.js.un~ b/.test.js.un~ new file mode 100644 index 0000000..01a1223 Binary files /dev/null and b/.test.js.un~ differ diff --git a/.test2.js.un~ b/.test2.js.un~ new file mode 100644 index 0000000..319c9a9 Binary files /dev/null and b/.test2.js.un~ 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 +* Web Audio API + +* a Flash fallback originally taken from dynamicaudio.js by Ben Firshman + 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 - @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 = ''; + 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 Binary files /dev/null and b/da.swf 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;iopenHeight){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-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;bb){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 @@ + + + + + + + + + + + + Chopin - Waltz Op.61 (Minute Waltz) | + Rachmaninov - Piano Concerto No.3 (First movement) + + 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 Binary files /dev/null and b/minute_waltz.mid differ diff --git a/rachmaninov3.mid b/rachmaninov3.mid new file mode 100644 index 0000000..196a535 Binary files /dev/null and b/rachmaninov3.mid 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 @@ + + + + + + + + + + go + + 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 -- cgit v1.2.3-70-g09d2