├── LICENSE ├── README ├── Rakefile ├── audio.js ├── da.swf ├── dynamicaudio.as ├── gui.min.js ├── index.html ├── midifile.js ├── minute_waltz.mid ├── rachmaninov3.mid ├── replayer.js ├── sandbox.html ├── stream.js └── synth.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Matt Westcott & Ben Firshman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * The names of its contributors may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | THIS PROJECT IS NO LONGER MAINTAINED. http://unmaintained.tech/ 2 | =============================================================== 3 | 4 | see jasmid.ts fork: https://github.com/pravdomil/jasmid.ts 5 | 6 | 7 | jasmid - A Javascript MIDI file reader and synthesiser 8 | 9 | Originally presented at BarCamp London 8, 13-14 November 2010 10 | 11 | Instructions: 12 | Open index.html in browser. Turn up volume. Click on link. 13 | 14 | Sound output is via one of the following mechanisms, according to what your 15 | browser supports: 16 | * Mozilla Audio Data API 17 | * Web Audio API 18 | 19 | * a Flash fallback originally taken from dynamicaudio.js by Ben Firshman 20 | and hacked around by me. 21 | 22 | 23 | The code: 24 | stream.js - helper library for reading a string as a stream of typed data 25 | midifile.js - parses the MIDI file format into a header and a list of tracks, 26 | each consisting of a list of event objects 27 | replayer.js - steps over the data structure generated by midifile.js and calls 28 | the appropriate operations on the synthesiser 29 | synth.js - audio synthesiser; generates waveforms according to tweakable 30 | parameters 31 | audio.js - passes the generated waveform to either the Audio Data API or the 32 | Flash fallback widget (da.swf) 33 | 34 | 35 | Limitations: 36 | * The only event types supported by replayer.js are note on, note off, tempo 37 | change and program change 38 | * There are currently only two instrument presets defined in synth.js - one for 39 | strings and a 'piano' one for everything else - and neither of them are 40 | particularly good (just a single volume-modulated sine wave). 41 | 42 | 43 | Matt Westcott - @gasmanic - http://matt.west.co.tt/ 44 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Replace this with the path to the mxmlc executable in the Flex SDK. 2 | MXMLC = '/Developer/SDKs/flex_sdk_4.0.0.14159/bin/mxmlc' 3 | 4 | task :default => "da.swf" 5 | 6 | desc "build the dynamicaudio SWF" 7 | file "da.swf" => "dynamicaudio.as" do 8 | sh %[ #{MXMLC} -use-network=false -o da.swf -file-specs "dynamicaudio.as" ] 9 | end 10 | -------------------------------------------------------------------------------- /audio.js: -------------------------------------------------------------------------------- 1 | var sampleRate = 44100; /* hard-coded in Flash player */ 2 | 3 | function AudioPlayer(generator, opts) { 4 | if (!opts) opts = {}; 5 | var latency = opts.latency || 1; 6 | var checkInterval = latency * 100 /* in ms */ 7 | 8 | var audioElement = new Audio(); 9 | var webkitAudio = window.AudioContext || window.webkitAudioContext; 10 | var requestStop = false; 11 | 12 | if (audioElement.mozSetup) { 13 | audioElement.mozSetup(2, sampleRate); /* channels, sample rate */ 14 | 15 | var buffer = []; /* data generated but not yet written */ 16 | var minBufferLength = latency * 2 * sampleRate; /* refill buffer when there are only this many elements remaining */ 17 | var bufferFillLength = Math.floor(latency * sampleRate); 18 | 19 | function checkBuffer() { 20 | if (buffer.length) { 21 | var written = audioElement.mozWriteAudio(buffer); 22 | buffer = buffer.slice(written); 23 | } 24 | if (buffer.length < minBufferLength && !generator.finished) { 25 | buffer = buffer.concat(generator.generate(bufferFillLength)); 26 | } 27 | if (!requestStop && (!generator.finished || buffer.length)) { 28 | setTimeout(checkBuffer, checkInterval); 29 | } 30 | } 31 | checkBuffer(); 32 | 33 | return { 34 | 'type': 'Firefox Audio', 35 | 'stop': function() { 36 | requestStop = true; 37 | } 38 | } 39 | } else if (webkitAudio) { 40 | // Uses Webkit Web Audio API if available 41 | var context = new webkitAudio(); 42 | sampleRate = context.sampleRate; 43 | 44 | var channelCount = 2; 45 | var bufferSize = 4096*4; // Higher for less gitches, lower for less latency 46 | 47 | var node = context.createScriptProcessor(bufferSize, 0, channelCount); 48 | 49 | node.onaudioprocess = function(e) { process(e) }; 50 | 51 | function process(e) { 52 | if (generator.finished) { 53 | node.disconnect(); 54 | return; 55 | } 56 | 57 | var dataLeft = e.outputBuffer.getChannelData(0); 58 | var dataRight = e.outputBuffer.getChannelData(1); 59 | 60 | var generate = generator.generate(bufferSize); 61 | 62 | for (var i = 0; i < bufferSize; ++i) { 63 | dataLeft[i] = generate[i*2]; 64 | dataRight[i] = generate[i*2+1]; 65 | } 66 | } 67 | 68 | // start 69 | node.connect(context.destination); 70 | 71 | return { 72 | 'stop': function() { 73 | // pause 74 | node.disconnect(); 75 | requestStop = true; 76 | }, 77 | 'type': 'Webkit Audio' 78 | } 79 | 80 | } else { 81 | // Fall back to creating flash player 82 | var c = document.createElement('div'); 83 | c.innerHTML = ''; 84 | document.body.appendChild(c); 85 | var swf = document.getElementById('da-swf'); 86 | 87 | var minBufferDuration = latency * 1000; /* refill buffer when there are only this many ms remaining */ 88 | var bufferFillLength = latency * sampleRate; 89 | 90 | function write(data) { 91 | var out = new Array(data.length); 92 | for (var i = data.length-1; i != 0; i--) { 93 | out[i] = Math.floor(data[i]*32768); 94 | } 95 | return swf.write(out.join(' ')); 96 | } 97 | 98 | function checkBuffer() { 99 | if (swf.bufferedDuration() < minBufferDuration) { 100 | write(generator.generate(bufferFillLength)); 101 | }; 102 | if (!requestStop && !generator.finished) setTimeout(checkBuffer, checkInterval); 103 | } 104 | 105 | function checkReady() { 106 | if (swf.write) { 107 | checkBuffer(); 108 | } else { 109 | setTimeout(checkReady, 10); 110 | } 111 | } 112 | checkReady(); 113 | 114 | return { 115 | 'stop': function() { 116 | swf.stop(); 117 | requestStop = true; 118 | }, 119 | 'bufferedDuration': function() { 120 | return swf.bufferedDuration(); 121 | }, 122 | 'type': 'Flash Audio' 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /da.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gasman/jasmid/e5134d8afb2af434b1fed8711303f7afa912095b/da.swf -------------------------------------------------------------------------------- /dynamicaudio.as: -------------------------------------------------------------------------------- 1 | package { 2 | import flash.display.Sprite; 3 | import flash.events.SampleDataEvent; 4 | import flash.external.ExternalInterface; 5 | import flash.media.Sound; 6 | import flash.media.SoundChannel; 7 | 8 | public class dynamicaudio extends Sprite { 9 | public var bufferSize:Number = 2048; // In samples 10 | public var sound:Sound; 11 | public var buffer:Array = []; 12 | public var channel:SoundChannel; 13 | public var writtenSampleCount:Number = 0; 14 | 15 | public function dynamicaudio() { 16 | ExternalInterface.addCallback('write', write); 17 | ExternalInterface.addCallback('stop', stop); 18 | ExternalInterface.addCallback('bufferedDuration', bufferedDuration); 19 | this.sound = new Sound(); 20 | this.sound.addEventListener( 21 | SampleDataEvent.SAMPLE_DATA, 22 | soundGenerator 23 | ); 24 | this.channel = this.sound.play(); 25 | } 26 | 27 | // Called from JavaScript to add samples to the buffer 28 | // Note we are using a space separated string of samples instead of an 29 | // array. Flash's stupid ExternalInterface passes every sample as XML, 30 | // which is incredibly expensive to encode/decode 31 | public function write(s:String):Number { 32 | var multiplier:Number = 1/32768; 33 | var alreadyBufferedDuration:Number = (this.writtenSampleCount + this.buffer.length/2) / 44.1; 34 | for each (var sample:String in s.split(" ")) { 35 | this.buffer.push(Number(sample)*multiplier); 36 | } 37 | return (this.channel ? alreadyBufferedDuration - this.channel.position : 0); 38 | } 39 | 40 | public function bufferedDuration():Number { 41 | // duration (in ms) of audio written to Flash so far = (writtenSampleCount * 1000 / sampleRate) 42 | // number of ms in Flash's buffer = (writtenSampleCount * 1000 / sampleRate) - this.channel.position 43 | // number of ms in our buffer = (this.buffer.length/2 * 1000 / sampleRate) 44 | // (/2 because buffer stores stereo data => 2 elements per sample) 45 | // for 44100Hz, x * 1000 / sampleRate => x / 44.1 46 | return (this.writtenSampleCount + this.buffer.length/2) / 44.1 - this.channel.position; 47 | } 48 | 49 | public function stop():void { 50 | this.channel.stop(); 51 | this.buffer = []; 52 | this.writtenSampleCount = 0; 53 | this.channel = this.sound.play(); 54 | } 55 | 56 | public function soundGenerator(event:SampleDataEvent):void { 57 | var i:int; 58 | 59 | // If we haven't got enough data, write 2048 samples of silence to 60 | // both channels, the minimum Flash allows 61 | if (this.buffer.length < this.bufferSize*2) { 62 | for (i = 0; i < 4096; i++) { 63 | event.data.writeFloat(0.0); 64 | } 65 | this.writtenSampleCount += 2048; 66 | return; 67 | } 68 | 69 | var count:Number = Math.min(this.buffer.length, 16384); 70 | 71 | for each (var sample:Number in this.buffer.slice(0, count)) { 72 | event.data.writeFloat(sample); 73 | } 74 | 75 | this.writtenSampleCount += count/2; 76 | this.buffer = this.buffer.slice(count, this.buffer.length); 77 | } 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /gui.min.js: -------------------------------------------------------------------------------- 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); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 69 | 70 | 71 | Chopin - Waltz Op.61 (Minute Waltz) | 72 | Rachmaninov - Piano Concerto No.3 (First movement) 73 | 74 | 75 | -------------------------------------------------------------------------------- /midifile.js: -------------------------------------------------------------------------------- 1 | /* 2 | class to parse the .mid file format 3 | (depends on stream.js) 4 | */ 5 | function MidiFile(data) { 6 | function readChunk(stream) { 7 | var id = stream.read(4); 8 | var length = stream.readInt32(); 9 | return { 10 | 'id': id, 11 | 'length': length, 12 | 'data': stream.read(length) 13 | }; 14 | } 15 | 16 | var lastEventTypeByte; 17 | 18 | function readEvent(stream) { 19 | var event = {}; 20 | event.deltaTime = stream.readVarInt(); 21 | var eventTypeByte = stream.readInt8(); 22 | if ((eventTypeByte & 0xf0) == 0xf0) { 23 | /* system / meta event */ 24 | if (eventTypeByte == 0xff) { 25 | /* meta event */ 26 | event.type = 'meta'; 27 | var subtypeByte = stream.readInt8(); 28 | var length = stream.readVarInt(); 29 | switch(subtypeByte) { 30 | case 0x00: 31 | event.subtype = 'sequenceNumber'; 32 | if (length != 2) throw "Expected length for sequenceNumber event is 2, got " + length; 33 | event.number = stream.readInt16(); 34 | return event; 35 | case 0x01: 36 | event.subtype = 'text'; 37 | event.text = stream.read(length); 38 | return event; 39 | case 0x02: 40 | event.subtype = 'copyrightNotice'; 41 | event.text = stream.read(length); 42 | return event; 43 | case 0x03: 44 | event.subtype = 'trackName'; 45 | event.text = stream.read(length); 46 | return event; 47 | case 0x04: 48 | event.subtype = 'instrumentName'; 49 | event.text = stream.read(length); 50 | return event; 51 | case 0x05: 52 | event.subtype = 'lyrics'; 53 | event.text = stream.read(length); 54 | return event; 55 | case 0x06: 56 | event.subtype = 'marker'; 57 | event.text = stream.read(length); 58 | return event; 59 | case 0x07: 60 | event.subtype = 'cuePoint'; 61 | event.text = stream.read(length); 62 | return event; 63 | case 0x20: 64 | event.subtype = 'midiChannelPrefix'; 65 | if (length != 1) throw "Expected length for midiChannelPrefix event is 1, got " + length; 66 | event.channel = stream.readInt8(); 67 | return event; 68 | case 0x2f: 69 | event.subtype = 'endOfTrack'; 70 | if (length != 0) throw "Expected length for endOfTrack event is 0, got " + length; 71 | return event; 72 | case 0x51: 73 | event.subtype = 'setTempo'; 74 | if (length != 3) throw "Expected length for setTempo event is 3, got " + length; 75 | event.microsecondsPerBeat = ( 76 | (stream.readInt8() << 16) 77 | + (stream.readInt8() << 8) 78 | + stream.readInt8() 79 | ) 80 | return event; 81 | case 0x54: 82 | event.subtype = 'smpteOffset'; 83 | if (length != 5) throw "Expected length for smpteOffset event is 5, got " + length; 84 | var hourByte = stream.readInt8(); 85 | event.frameRate = { 86 | 0x00: 24, 0x20: 25, 0x40: 29, 0x60: 30 87 | }[hourByte & 0x60]; 88 | event.hour = hourByte & 0x1f; 89 | event.min = stream.readInt8(); 90 | event.sec = stream.readInt8(); 91 | event.frame = stream.readInt8(); 92 | event.subframe = stream.readInt8(); 93 | return event; 94 | case 0x58: 95 | event.subtype = 'timeSignature'; 96 | if (length != 4) throw "Expected length for timeSignature event is 4, got " + length; 97 | event.numerator = stream.readInt8(); 98 | event.denominator = Math.pow(2, stream.readInt8()); 99 | event.metronome = stream.readInt8(); 100 | event.thirtyseconds = stream.readInt8(); 101 | return event; 102 | case 0x59: 103 | event.subtype = 'keySignature'; 104 | if (length != 2) throw "Expected length for keySignature event is 2, got " + length; 105 | event.key = stream.readInt8(true); 106 | event.scale = stream.readInt8(); 107 | return event; 108 | case 0x7f: 109 | event.subtype = 'sequencerSpecific'; 110 | event.data = stream.read(length); 111 | return event; 112 | default: 113 | // console.log("Unrecognised meta event subtype: " + subtypeByte); 114 | event.subtype = 'unknown' 115 | event.data = stream.read(length); 116 | return event; 117 | } 118 | event.data = stream.read(length); 119 | return event; 120 | } else if (eventTypeByte == 0xf0) { 121 | event.type = 'sysEx'; 122 | var length = stream.readVarInt(); 123 | event.data = stream.read(length); 124 | return event; 125 | } else if (eventTypeByte == 0xf7) { 126 | event.type = 'dividedSysEx'; 127 | var length = stream.readVarInt(); 128 | event.data = stream.read(length); 129 | return event; 130 | } else { 131 | throw "Unrecognised MIDI event type byte: " + eventTypeByte; 132 | } 133 | } else { 134 | /* channel event */ 135 | var param1; 136 | if ((eventTypeByte & 0x80) == 0) { 137 | /* running status - reuse lastEventTypeByte as the event type. 138 | eventTypeByte is actually the first parameter 139 | */ 140 | param1 = eventTypeByte; 141 | eventTypeByte = lastEventTypeByte; 142 | } else { 143 | param1 = stream.readInt8(); 144 | lastEventTypeByte = eventTypeByte; 145 | } 146 | var eventType = eventTypeByte >> 4; 147 | event.channel = eventTypeByte & 0x0f; 148 | event.type = 'channel'; 149 | switch (eventType) { 150 | case 0x08: 151 | event.subtype = 'noteOff'; 152 | event.noteNumber = param1; 153 | event.velocity = stream.readInt8(); 154 | return event; 155 | case 0x09: 156 | event.noteNumber = param1; 157 | event.velocity = stream.readInt8(); 158 | if (event.velocity == 0) { 159 | event.subtype = 'noteOff'; 160 | } else { 161 | event.subtype = 'noteOn'; 162 | } 163 | return event; 164 | case 0x0a: 165 | event.subtype = 'noteAftertouch'; 166 | event.noteNumber = param1; 167 | event.amount = stream.readInt8(); 168 | return event; 169 | case 0x0b: 170 | event.subtype = 'controller'; 171 | event.controllerType = param1; 172 | event.value = stream.readInt8(); 173 | return event; 174 | case 0x0c: 175 | event.subtype = 'programChange'; 176 | event.programNumber = param1; 177 | return event; 178 | case 0x0d: 179 | event.subtype = 'channelAftertouch'; 180 | event.amount = param1; 181 | return event; 182 | case 0x0e: 183 | event.subtype = 'pitchBend'; 184 | event.value = param1 + (stream.readInt8() << 7); 185 | return event; 186 | default: 187 | throw "Unrecognised MIDI event type: " + eventType 188 | /* 189 | console.log("Unrecognised MIDI event type: " + eventType); 190 | stream.readInt8(); 191 | event.subtype = 'unknown'; 192 | return event; 193 | */ 194 | } 195 | } 196 | } 197 | 198 | stream = Stream(data); 199 | var headerChunk = readChunk(stream); 200 | if (headerChunk.id != 'MThd' || headerChunk.length != 6) { 201 | throw "Bad .mid file - header not found"; 202 | } 203 | var headerStream = Stream(headerChunk.data); 204 | var formatType = headerStream.readInt16(); 205 | var trackCount = headerStream.readInt16(); 206 | var timeDivision = headerStream.readInt16(); 207 | 208 | if (timeDivision & 0x8000) { 209 | throw "Expressing time division in SMTPE frames is not supported yet" 210 | } else { 211 | ticksPerBeat = timeDivision; 212 | } 213 | 214 | var header = { 215 | 'formatType': formatType, 216 | 'trackCount': trackCount, 217 | 'ticksPerBeat': ticksPerBeat 218 | } 219 | var tracks = []; 220 | for (var i = 0; i < header.trackCount; i++) { 221 | tracks[i] = []; 222 | var trackChunk = readChunk(stream); 223 | if (trackChunk.id != 'MTrk') { 224 | throw "Unexpected chunk - expected MTrk, got "+ trackChunk.id; 225 | } 226 | var trackStream = Stream(trackChunk.data); 227 | while (!trackStream.eof()) { 228 | var event = readEvent(trackStream); 229 | tracks[i].push(event); 230 | //console.log(event); 231 | } 232 | } 233 | 234 | return { 235 | 'header': header, 236 | 'tracks': tracks 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /minute_waltz.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gasman/jasmid/e5134d8afb2af434b1fed8711303f7afa912095b/minute_waltz.mid -------------------------------------------------------------------------------- /rachmaninov3.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gasman/jasmid/e5134d8afb2af434b1fed8711303f7afa912095b/rachmaninov3.mid -------------------------------------------------------------------------------- /replayer.js: -------------------------------------------------------------------------------- 1 | function Replayer(midiFile, synth) { 2 | var trackStates = []; 3 | var beatsPerMinute = 120; 4 | var ticksPerBeat = midiFile.header.ticksPerBeat; 5 | var channelCount = 16; 6 | 7 | for (var i = 0; i < midiFile.tracks.length; i++) { 8 | trackStates[i] = { 9 | 'nextEventIndex': 0, 10 | 'ticksToNextEvent': ( 11 | midiFile.tracks[i].length ? 12 | midiFile.tracks[i][0].deltaTime : 13 | null 14 | ) 15 | }; 16 | } 17 | 18 | function Channel() { 19 | 20 | var generatorsByNote = {}; 21 | var currentProgram = PianoProgram; 22 | 23 | function noteOn(note, velocity) { 24 | if (generatorsByNote[note] && !generatorsByNote[note].released) { 25 | /* playing same note before releasing the last one. BOO */ 26 | generatorsByNote[note].noteOff(); /* TODO: check whether we ought to be passing a velocity in */ 27 | } 28 | generator = currentProgram.createNote(note, velocity); 29 | synth.addGenerator(generator); 30 | generatorsByNote[note] = generator; 31 | } 32 | function noteOff(note, velocity) { 33 | if (generatorsByNote[note] && !generatorsByNote[note].released) { 34 | generatorsByNote[note].noteOff(velocity); 35 | } 36 | } 37 | function setProgram(programNumber) { 38 | currentProgram = PROGRAMS[programNumber] || PianoProgram; 39 | } 40 | 41 | return { 42 | 'noteOn': noteOn, 43 | 'noteOff': noteOff, 44 | 'setProgram': setProgram 45 | } 46 | } 47 | 48 | var channels = []; 49 | for (var i = 0; i < channelCount; i++) { 50 | channels[i] = Channel(); 51 | } 52 | 53 | var nextEventInfo; 54 | var samplesToNextEvent = 0; 55 | 56 | function getNextEvent() { 57 | var ticksToNextEvent = null; 58 | var nextEventTrack = null; 59 | var nextEventIndex = null; 60 | 61 | for (var i = 0; i < trackStates.length; i++) { 62 | if ( 63 | trackStates[i].ticksToNextEvent != null 64 | && (ticksToNextEvent == null || trackStates[i].ticksToNextEvent < ticksToNextEvent) 65 | ) { 66 | ticksToNextEvent = trackStates[i].ticksToNextEvent; 67 | nextEventTrack = i; 68 | nextEventIndex = trackStates[i].nextEventIndex; 69 | } 70 | } 71 | if (nextEventTrack != null) { 72 | /* consume event from that track */ 73 | var nextEvent = midiFile.tracks[nextEventTrack][nextEventIndex]; 74 | if (midiFile.tracks[nextEventTrack][nextEventIndex + 1]) { 75 | trackStates[nextEventTrack].ticksToNextEvent += midiFile.tracks[nextEventTrack][nextEventIndex + 1].deltaTime; 76 | } else { 77 | trackStates[nextEventTrack].ticksToNextEvent = null; 78 | } 79 | trackStates[nextEventTrack].nextEventIndex += 1; 80 | /* advance timings on all tracks by ticksToNextEvent */ 81 | for (var i = 0; i < trackStates.length; i++) { 82 | if (trackStates[i].ticksToNextEvent != null) { 83 | trackStates[i].ticksToNextEvent -= ticksToNextEvent 84 | } 85 | } 86 | nextEventInfo = { 87 | 'ticksToEvent': ticksToNextEvent, 88 | 'event': nextEvent, 89 | 'track': nextEventTrack 90 | } 91 | var beatsToNextEvent = ticksToNextEvent / ticksPerBeat; 92 | var secondsToNextEvent = beatsToNextEvent / (beatsPerMinute / 60); 93 | samplesToNextEvent += secondsToNextEvent * synth.sampleRate; 94 | } else { 95 | nextEventInfo = null; 96 | samplesToNextEvent = null; 97 | self.finished = true; 98 | } 99 | } 100 | 101 | getNextEvent(); 102 | 103 | function generate(samples) { 104 | var data = new Array(samples*2); 105 | var samplesRemaining = samples; 106 | var dataOffset = 0; 107 | 108 | while (true) { 109 | if (samplesToNextEvent != null && samplesToNextEvent <= samplesRemaining) { 110 | /* generate samplesToNextEvent samples, process event and repeat */ 111 | var samplesToGenerate = Math.ceil(samplesToNextEvent); 112 | if (samplesToGenerate > 0) { 113 | synth.generateIntoBuffer(samplesToGenerate, data, dataOffset); 114 | dataOffset += samplesToGenerate * 2; 115 | samplesRemaining -= samplesToGenerate; 116 | samplesToNextEvent -= samplesToGenerate; 117 | } 118 | 119 | handleEvent(); 120 | getNextEvent(); 121 | } else { 122 | /* generate samples to end of buffer */ 123 | if (samplesRemaining > 0) { 124 | synth.generateIntoBuffer(samplesRemaining, data, dataOffset); 125 | samplesToNextEvent -= samplesRemaining; 126 | } 127 | break; 128 | } 129 | } 130 | return data; 131 | } 132 | 133 | function handleEvent() { 134 | var event = nextEventInfo.event; 135 | switch (event.type) { 136 | case 'meta': 137 | switch (event.subtype) { 138 | case 'setTempo': 139 | beatsPerMinute = 60000000 / event.microsecondsPerBeat 140 | } 141 | break; 142 | case 'channel': 143 | switch (event.subtype) { 144 | case 'noteOn': 145 | channels[event.channel].noteOn(event.noteNumber, event.velocity); 146 | break; 147 | case 'noteOff': 148 | channels[event.channel].noteOff(event.noteNumber, event.velocity); 149 | break; 150 | case 'programChange': 151 | //console.log('program change to ' + event.programNumber); 152 | channels[event.channel].setProgram(event.programNumber); 153 | break; 154 | } 155 | break; 156 | } 157 | } 158 | 159 | function replay(audio) { 160 | console.log('replay'); 161 | audio.write(generate(44100)); 162 | setTimeout(function() {replay(audio)}, 10); 163 | } 164 | 165 | var self = { 166 | 'replay': replay, 167 | 'generate': generate, 168 | 'finished': false 169 | } 170 | return self; 171 | } 172 | -------------------------------------------------------------------------------- /sandbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 75 | 76 | 77 | go 78 | 79 | 80 | -------------------------------------------------------------------------------- /stream.js: -------------------------------------------------------------------------------- 1 | /* Wrapper for accessing strings through sequential reads */ 2 | function Stream(str) { 3 | var position = 0; 4 | 5 | function read(length) { 6 | var result = str.substr(position, length); 7 | position += length; 8 | return result; 9 | } 10 | 11 | /* read a big-endian 32-bit integer */ 12 | function readInt32() { 13 | var result = ( 14 | (str.charCodeAt(position) << 24) 15 | + (str.charCodeAt(position + 1) << 16) 16 | + (str.charCodeAt(position + 2) << 8) 17 | + str.charCodeAt(position + 3)); 18 | position += 4; 19 | return result; 20 | } 21 | 22 | /* read a big-endian 16-bit integer */ 23 | function readInt16() { 24 | var result = ( 25 | (str.charCodeAt(position) << 8) 26 | + str.charCodeAt(position + 1)); 27 | position += 2; 28 | return result; 29 | } 30 | 31 | /* read an 8-bit integer */ 32 | function readInt8(signed) { 33 | var result = str.charCodeAt(position); 34 | if (signed && result > 127) result -= 256; 35 | position += 1; 36 | return result; 37 | } 38 | 39 | function eof() { 40 | return position >= str.length; 41 | } 42 | 43 | /* read a MIDI-style variable-length integer 44 | (big-endian value in groups of 7 bits, 45 | with top bit set to signify that another byte follows) 46 | */ 47 | function readVarInt() { 48 | var result = 0; 49 | while (true) { 50 | var b = readInt8(); 51 | if (b & 0x80) { 52 | result += (b & 0x7f); 53 | result <<= 7; 54 | } else { 55 | /* b is the last byte */ 56 | return result + b; 57 | } 58 | } 59 | } 60 | 61 | return { 62 | 'eof': eof, 63 | 'read': read, 64 | 'readInt32': readInt32, 65 | 'readInt16': readInt16, 66 | 'readInt8': readInt8, 67 | 'readVarInt': readVarInt 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /synth.js: -------------------------------------------------------------------------------- 1 | function SineGenerator(freq) { 2 | var self = {'alive': true}; 3 | var period = sampleRate / freq; 4 | var t = 0; 5 | 6 | self.generate = function(buf, offset, count) { 7 | for (; count; count--) { 8 | var phase = t / period; 9 | var result = Math.sin(phase * 2 * Math.PI); 10 | buf[offset++] += result; 11 | buf[offset++] += result; 12 | t++; 13 | } 14 | } 15 | 16 | return self; 17 | } 18 | 19 | function SquareGenerator(freq, phase) { 20 | var self = {'alive': true}; 21 | var period = sampleRate / freq; 22 | var t = 0; 23 | 24 | self.generate = function(buf, offset, count) { 25 | for (; count; count--) { 26 | var result = ( (t / period) % 1 > phase ? 1 : -1); 27 | buf[offset++] += result; 28 | buf[offset++] += result; 29 | t++; 30 | } 31 | } 32 | 33 | return self; 34 | } 35 | 36 | function ADSRGenerator(child, attackAmplitude, sustainAmplitude, attackTimeS, decayTimeS, releaseTimeS) { 37 | var self = {'alive': true} 38 | var attackTime = sampleRate * attackTimeS; 39 | var decayTime = sampleRate * (attackTimeS + decayTimeS); 40 | var decayRate = (attackAmplitude - sustainAmplitude) / (decayTime - attackTime); 41 | var releaseTime = null; /* not known yet */ 42 | var endTime = null; /* not known yet */ 43 | var releaseRate = sustainAmplitude / (sampleRate * releaseTimeS); 44 | var t = 0; 45 | 46 | self.noteOff = function() { 47 | if (self.released) return; 48 | releaseTime = t; 49 | self.released = true; 50 | endTime = releaseTime + sampleRate * releaseTimeS; 51 | } 52 | 53 | self.generate = function(buf, offset, count) { 54 | if (!self.alive) return; 55 | var input = new Array(count * 2); 56 | for (var i = 0; i < count*2; i++) { 57 | input[i] = 0; 58 | } 59 | child.generate(input, 0, count); 60 | 61 | childOffset = 0; 62 | while(count) { 63 | if (releaseTime != null) { 64 | if (t < endTime) { 65 | /* release */ 66 | while(count && t < endTime) { 67 | var ampl = sustainAmplitude - releaseRate * (t - releaseTime); 68 | buf[offset++] += input[childOffset++] * ampl; 69 | buf[offset++] += input[childOffset++] * ampl; 70 | t++; 71 | count--; 72 | } 73 | } else { 74 | /* dead */ 75 | self.alive = false; 76 | return; 77 | } 78 | } else if (t < attackTime) { 79 | /* attack */ 80 | while(count && t < attackTime) { 81 | var ampl = attackAmplitude * t / attackTime; 82 | buf[offset++] += input[childOffset++] * ampl; 83 | buf[offset++] += input[childOffset++] * ampl; 84 | t++; 85 | count--; 86 | } 87 | } else if (t < decayTime) { 88 | /* decay */ 89 | while(count && t < decayTime) { 90 | var ampl = attackAmplitude - decayRate * (t - attackTime); 91 | buf[offset++] += input[childOffset++] * ampl; 92 | buf[offset++] += input[childOffset++] * ampl; 93 | t++; 94 | count--; 95 | } 96 | } else { 97 | /* sustain */ 98 | while(count) { 99 | buf[offset++] += input[childOffset++] * sustainAmplitude; 100 | buf[offset++] += input[childOffset++] * sustainAmplitude; 101 | t++; 102 | count--; 103 | } 104 | } 105 | } 106 | } 107 | 108 | return self; 109 | } 110 | 111 | function midiToFrequency(note) { 112 | return 440 * Math.pow(2, (note-69)/12); 113 | } 114 | 115 | PianoProgram = { 116 | 'attackAmplitude': 0.2, 117 | 'sustainAmplitude': 0.1, 118 | 'attackTime': 0.02, 119 | 'decayTime': 0.3, 120 | 'releaseTime': 0.02, 121 | 'createNote': function(note, velocity) { 122 | var frequency = midiToFrequency(note); 123 | return ADSRGenerator( 124 | SineGenerator(frequency), 125 | this.attackAmplitude * (velocity / 128), this.sustainAmplitude * (velocity / 128), 126 | this.attackTime, this.decayTime, this.releaseTime 127 | ); 128 | } 129 | } 130 | 131 | StringProgram = { 132 | 'createNote': function(note, velocity) { 133 | var frequency = midiToFrequency(note); 134 | return ADSRGenerator( 135 | SineGenerator(frequency), 136 | 0.5 * (velocity / 128), 0.2 * (velocity / 128), 137 | 0.4, 0.8, 0.4 138 | ); 139 | } 140 | } 141 | 142 | PROGRAMS = { 143 | 41: StringProgram, 144 | 42: StringProgram, 145 | 43: StringProgram, 146 | 44: StringProgram, 147 | 45: StringProgram, 148 | 46: StringProgram, 149 | 47: StringProgram, 150 | 49: StringProgram, 151 | 50: StringProgram 152 | }; 153 | 154 | function Synth(sampleRate) { 155 | 156 | var generators = []; 157 | 158 | function addGenerator(generator) { 159 | generators.push(generator); 160 | } 161 | 162 | function generate(samples) { 163 | var data = new Array(samples*2); 164 | generateIntoBuffer(samples, data, 0); 165 | return data; 166 | } 167 | 168 | function generateIntoBuffer(samplesToGenerate, buffer, offset) { 169 | for (var i = offset; i < offset + samplesToGenerate * 2; i++) { 170 | buffer[i] = 0; 171 | } 172 | for (var i = generators.length - 1; i >= 0; i--) { 173 | generators[i].generate(buffer, offset, samplesToGenerate); 174 | if (!generators[i].alive) generators.splice(i, 1); 175 | } 176 | } 177 | 178 | return { 179 | 'sampleRate': sampleRate, 180 | 'addGenerator': addGenerator, 181 | 'generate': generate, 182 | 'generateIntoBuffer': generateIntoBuffer 183 | } 184 | } 185 | --------------------------------------------------------------------------------