Advertisement
Guest User

effect_chain_mixer.js

a guest
Sep 6th, 2013
267
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. FILE_KEYWORD = "file";
  2. FILE_KEYWORD_LENGTH = FILE_KEYWORD.length;
  3. FILE_LOAD_WAIT = 100;   // time to wait between pollings of EffectChainMixer.loadingFiles > 0
  4.  
  5. window.AudioContext = window.AudioContext || window.webkitAudioContext;
  6. CONTEXT = new AudioContext();
  7.  
  8. console.log("loaded effect_chain_mixer.js");
  9.  
  10. // construct the EffectChainMixer by reading the container objects and preparing for playback
  11. function EffectChainMixer (CONTAINER_FILE) {
  12.     console.log("constructor call");
  13.     this.containerDict = {};
  14.     this.fileSources = {};
  15.     this.audioNodes = {};
  16.     this.loadingFiles = 0;
  17.     this.times = {songPos : 0, contextTime : CONTEXT.currentTime, songLength : 0};
  18.     this.isPlaying = false;
  19.    
  20.     if (CONTAINER_FILE && typeof CONTAINER_FILE === 'string') {
  21.         this.containerDict = JSON.parse(CONTAINER_FILE);
  22.     }
  23.     else {
  24.         console.error("There is a problem with the container file!");
  25.     }
  26. }
  27.  
  28. // start playback
  29. EffectChainMixer.prototype.init = function() {
  30.     this.createAudioNodes();
  31.     this.connectAudioNodes();
  32. };
  33.  
  34. // find file sources and add them to the fileSources list
  35. EffectChainMixer.prototype.createAudioNodes = function() {
  36.     console.log("creating AudioNodes");
  37.     var containerID, container, sourceID, isFileSource;
  38.    
  39.     for (containerID in this.containerDict) {
  40.         console.log("processing container " + containerID);
  41.         container = clone(this.containerDict[containerID]);
  42.         // iterate trough all sources of the container
  43.         // file sources should have ONLY ONE source: the file itself
  44.         for (sourceID in container.sources) {
  45.             isFileSource = false;
  46.             console.log("processing source " + sourceID);
  47.             // <source_id> starts with FILE_KEYWORD, so we have a file as source
  48.             if (sourceID.slice(0, FILE_KEYWORD_LENGTH) == FILE_KEYWORD ) {
  49.                 this.fileSources[containerID] = container;
  50.                 isFileSource = true;
  51.                 console.log("found fileSource");
  52.             }
  53.             // sources with volume other than 1.0 need hidden gain containers
  54.             // NOTE file sources always have a hidden gain container
  55.             if (container.sources[sourceID].volume != 1 || isFileSource) {
  56.                 console.log("source has volume !=1 or is a file");
  57.                 this.createAudioNode(sourceID+"to"+containerID,"gain");
  58.                 this.applyParameters(sourceID+"to"+containerID,{"gain" : container.sources[sourceID].volume});
  59.             }
  60.         }
  61.         // now that we've checked for source files and hidden gain nodes, create the AudioNode and apply the parameters
  62.         this.createAudioNode(containerID, container.effect.effect_id);
  63.         // special threatment for fileSources, they are created asyncroniously and do not yet exist
  64.         if (!isFileSource) {
  65.             this.applyParameters(containerID, container.effect.timepoints[0].change);
  66.         }
  67.     }
  68.     // create a master gain container and connect it to the audio output
  69.     this.createAudioNode("MASTER_GAIN","gain");
  70.     this.audioNodes["MASTER_GAIN"].connect(CONTEXT.destination);
  71.     // set master output to explicit 2-channels (stereo)
  72.     this.audioNodes["MASTER_GAIN"].channelCount = 2;
  73.     this.audioNodes["MASTER_GAIN"].channelCountMode = "explicit";
  74.     this.audioNodes["MASTER_GAIN"].channelInterpretation = "speakers";
  75.     this.audioNodes["MASTER_GAIN"].volume = 1.0;
  76. };
  77.  
  78. EffectChainMixer.prototype.createAudioNode = function (containerID, effectID) {
  79.     console.log("creating "+ effectID +" AudioNode with containerID " + containerID);
  80.     switch (effectID) {
  81.             case "delay":
  82.                  this.audioNodes[containerID] = CONTEXT.createDelay();
  83.                  break;
  84.             case "eq":
  85.                 this.audioNodes[containerID] = CONTEXT.createBiquadFilter();
  86.                 break;
  87.             case "pan":
  88.                 this.audioNodes[containerID] = CONTEXT.createPanner();
  89.                 break;
  90.             case "compressor":
  91.                 this.audioNodes[containerID] = CONTEXT.createDynamicsCompressor();
  92.                 break;
  93.             case "gain":
  94.                 this.audioNodes[containerID] = CONTEXT.createGain();
  95.                 break;
  96.             case "clip":
  97.                 this.createFileSourceNode(containerID);
  98.                 break;
  99.             default:
  100.                 console.error("Unknown effect " + effect_id);
  101.                 break;
  102.         }
  103. };
  104.  
  105. // apply parameters to an AudiNode
  106. // NOTE gain changes for file sources and hidden gain containers need the right containerID of the actual gain container
  107. EffectChainMixer.prototype.applyParameters = function(containerID, parameters) {
  108.     console.log("applying parameters");
  109.     // special threatment for panner nodes
  110.     if (typeof this.audioNodes[containerID] == "PannerNode") {
  111.         var pos;
  112.         this.audioNodes[containerID].panningModel =params.model;
  113.         if (parameters.type == "2D") {
  114.                 pos = panPos(parameters.value);
  115.         }
  116.         else {
  117.                 pos = parameters.pos;
  118.         }
  119.         this.audioNodes[containerID].setPosition(pos.x, pos.y, pos.z);
  120.     }
  121.     else {
  122.         for (param in parameters) {
  123.             // special threament for mode changes
  124.             if (param == "mode") {
  125.                 // TODO: implement mode switches
  126.                 switch (parameters[param]) {
  127.                     case "normal":
  128.                     case "mute":
  129.                     case "solo":
  130.                 }
  131.             }
  132.             console.log("parameter: " + param + ", value: " + parameters[param].value);
  133.             console.log("node " + this.audioNodes[containerID]);
  134.             this.audioNodes[containerID][param] = parameters[param].value;
  135.         }
  136.     }
  137. };
  138.  
  139. // creates an AudioBufferSourceNode after loading and decoding the audio file
  140. EffectChainMixer.prototype.createFileSourceNode = function(containerID) {
  141.     // increase counter of files loading
  142.     this.loadingFiles += 1;
  143.     console.log("creating fileSourceNode");
  144.     var that, url, sourceID, request, buffer;
  145.     console.log("container "+containerID+": " + this.containerDict[containerID]);
  146.     that = this;
  147.    
  148.     for (sourceID in this.containerDict[containerID].sources) {
  149.         url = sourceID.slice(FILE_KEYWORD_LENGTH);
  150.     }
  151.     request = new XMLHttpRequest();
  152.     request.onerror = function(error) { console.error('XMLHttpRequest error: ' + error) };
  153.     request.open('GET', url, true);
  154.     request.responseType = 'arraybuffer';
  155.     // decode asynchronously on load
  156.     request.onload = function() {
  157.         CONTEXT.decodeAudioData(request.response, function(buffer) {
  158.             if (!buffer) {
  159.                 console.error('error decoding file data: ' + url);
  160.             return;
  161.             }
  162.             that.audioNodes[containerID] = CONTEXT.createBufferSource();
  163.             that.audioNodes[containerID].buffer = buffer;
  164.             that.audioNodes[containerID].mode = "normal";
  165.             that.applyParameters(containerID, that.containerDict[containerID].effect.timepoints[0].change);
  166.             length = that.audioNodes[containerID].buffer.duration+that.audioNodes[containerID].offset;
  167.             if (length > that.times.songLength) {
  168.                 console.log("NEW SONGLENGTH: " + length);
  169.                     that.times.songLength = length;
  170.             }
  171.             console.log("finished loading " +url);
  172.             // decrease counter of files loading
  173.             that.loadingFiles -= 1;
  174.         },
  175.         function(error) {console.error('decodeAudioData error', error); });
  176.     };
  177.     request.send();
  178. };
  179.  
  180. // connect all AudioNodes
  181. EffectChainMixer.prototype.connectAudioNodes = function() {
  182.     console.log("connecting AudioNodes");
  183.     // wait for all files to be loaded
  184.     if (this.loadingFiles > 0) {
  185.         var that = this;
  186.         console.log("waiting for " + this.loadingFiles + " loading files..");
  187.         setTimeout(function(){that.connectAudioNodes(); },FILE_LOAD_WAIT);
  188.     }
  189.     else{
  190.         var containerID, container, consumerID, fileSourceID, sourceContainerID, hasConsumer;
  191.        
  192.         for (containerID in this.containerDict) {
  193.             container = clone(this.containerDict[containerID]);
  194.             sourceContainerID = containerID;
  195.             hasConsumer = false;
  196.            
  197.             // check if source of the container is a file and connect it to its gain container
  198.             if (containerID in this.fileSources) {
  199.                 for (fileSourceID in container.sources) {
  200.                     // the gain container is the new source for consumers
  201.                     sourceContainerID = fileSourceID+"to"+containerID;
  202.                     console.log("TYPEOF " + containerID + ": " + typeof(this.audioNodes[containerID]));
  203.                     this.audioNodes[containerID].connect(this.audioNodes[sourceContainerID]);
  204.                 }
  205.             }
  206.             // iterate trough all consumers of the container
  207.             for (consumerID in container.consumers) {
  208.                 hasConsumer = true;
  209.                 // check for hidden gain container between this container and the consumer
  210.                 // connect the hidden gain container to the actuall container, then redirect
  211.                 // the output of the source to the hidden gain container
  212.                 if (this.containerDict[consumerID].sources[containerId].volume != 1) {
  213.                     this.audioNodes[containerID + "to" + consumerID].connect(consumerID);
  214.                     consumerID = containerID + "to" + consumerID;
  215.                 }
  216.                 this.audioNodes[sourceContainerID].connect(consumerID);        
  217.             }
  218.             // if the container has no consumer, connect it to the master output
  219.             if (!hasConsumer) {
  220.                 this.audioNodes[sourceContainerID].connect(this.audioNodes["MASTER_GAIN"]);
  221.             }
  222.         }
  223.     }
  224. };
  225.  
  226. // start playback
  227. EffectChainMixer.prototype.play = function() {
  228.     if (!this.isPlaying) {
  229.         // wait for all files to be loaded
  230.         if (this.loadingFiles > 0) {
  231.             var that = this;
  232.             console.log("waiting for " + this.loadingFiles + " loading files..");
  233.             setTimeout(function(){that.play();},FILE_LOAD_WAIT);
  234.         }
  235.         else {
  236.             this.connectAudioNodes();
  237.             this.isPlaying = true;
  238.             var currentTime = CONTEXT.currentTime;
  239.             this.times.contextTime = currentTime;
  240.             for (sourceID in this.fileSources) {
  241.                 console.log("starting playback of "+sourceID);
  242.                 params = this.fileSources[sourceID].effect.timepoints[0].change;
  243.                 this.audioNodes[sourceID].start(params.offset.value + currentTime, params.trim.value + this.times.songPos, params.duration.value);
  244.             }
  245.         }
  246.     }
  247. };
  248.  
  249. // pause playback
  250. EffectChainMixer.prototype.pause = function() {
  251.     if (this.isPlaying) {
  252.         var now = CONTEXT.currentTime;
  253.         this.stop();
  254.         this.times.songPos = now - this.times.contextTime;
  255.        
  256.     }  
  257. };
  258.  
  259. // seek to certain point in track
  260. EffectChainMixer.prototype.seek = function(time) {
  261.     this.stop();
  262.     this.times.songPos = time;
  263.     this.play();
  264. };
  265.  
  266. // pause playback and reset to start of track
  267. EffectChainMixer.prototype.stop = function() {
  268.     if (this.isPlaying) {
  269.         for (sourceID in this.fileSources) {
  270.             this.audioNodes[sourceID].noteOff(0);
  271.         }
  272.         this.isPlaying = false;
  273.     }
  274.     this.times.songPos = 0;
  275. };
  276.  
  277.  
  278. /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  279. // helper functions
  280. /////////////////////////////////////////////////////////////////////////////////////////////////////////////
  281.  
  282. // fastest possible object cloning,called "Cached Crockford"
  283. function F() {}
  284. function clone(o) {
  285.     F.prototype = o;
  286.     return new F();
  287. }
  288.  
  289.  
  290. // calculate position for 2D panning
  291. function panPos(val) {
  292.     var xDeg = val;
  293.     var zDeg = xDeg + 90;
  294.     if (zDeg > 90) {
  295.         zDeg = 180 - zDeg;
  296.     }
  297.     var x = Math.sin(xDeg * (Math.PI / 180));
  298.     var z = Math.sin(zDeg * (Math.PI / 180));
  299.     return {"x" : x , "y" : 0,  "z" :  z};
  300. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement