Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- FILE_KEYWORD = "file";
- FILE_KEYWORD_LENGTH = FILE_KEYWORD.length;
- FILE_LOAD_WAIT = 100; // time to wait between pollings of EffectChainMixer.loadingFiles > 0
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
- CONTEXT = new AudioContext();
- console.log("loaded effect_chain_mixer.js");
- // construct the EffectChainMixer by reading the container objects and preparing for playback
- function EffectChainMixer (CONTAINER_FILE) {
- console.log("constructor call");
- this.containerDict = {};
- this.fileSources = {};
- this.audioNodes = {};
- this.loadingFiles = 0;
- this.times = {songPos : 0, contextTime : CONTEXT.currentTime, songLength : 0};
- this.isPlaying = false;
- if (CONTAINER_FILE && typeof CONTAINER_FILE === 'string') {
- this.containerDict = JSON.parse(CONTAINER_FILE);
- }
- else {
- console.error("There is a problem with the container file!");
- }
- }
- // start playback
- EffectChainMixer.prototype.init = function() {
- this.createAudioNodes();
- this.connectAudioNodes();
- };
- // find file sources and add them to the fileSources list
- EffectChainMixer.prototype.createAudioNodes = function() {
- console.log("creating AudioNodes");
- var containerID, container, sourceID, isFileSource;
- for (containerID in this.containerDict) {
- console.log("processing container " + containerID);
- container = clone(this.containerDict[containerID]);
- // iterate trough all sources of the container
- // file sources should have ONLY ONE source: the file itself
- for (sourceID in container.sources) {
- isFileSource = false;
- console.log("processing source " + sourceID);
- // <source_id> starts with FILE_KEYWORD, so we have a file as source
- if (sourceID.slice(0, FILE_KEYWORD_LENGTH) == FILE_KEYWORD ) {
- this.fileSources[containerID] = container;
- isFileSource = true;
- console.log("found fileSource");
- }
- // sources with volume other than 1.0 need hidden gain containers
- // NOTE file sources always have a hidden gain container
- if (container.sources[sourceID].volume != 1 || isFileSource) {
- console.log("source has volume !=1 or is a file");
- this.createAudioNode(sourceID+"to"+containerID,"gain");
- this.applyParameters(sourceID+"to"+containerID,{"gain" : container.sources[sourceID].volume});
- }
- }
- // now that we've checked for source files and hidden gain nodes, create the AudioNode and apply the parameters
- this.createAudioNode(containerID, container.effect.effect_id);
- // special threatment for fileSources, they are created asyncroniously and do not yet exist
- if (!isFileSource) {
- this.applyParameters(containerID, container.effect.timepoints[0].change);
- }
- }
- // create a master gain container and connect it to the audio output
- this.createAudioNode("MASTER_GAIN","gain");
- this.audioNodes["MASTER_GAIN"].connect(CONTEXT.destination);
- // set master output to explicit 2-channels (stereo)
- this.audioNodes["MASTER_GAIN"].channelCount = 2;
- this.audioNodes["MASTER_GAIN"].channelCountMode = "explicit";
- this.audioNodes["MASTER_GAIN"].channelInterpretation = "speakers";
- this.audioNodes["MASTER_GAIN"].volume = 1.0;
- };
- EffectChainMixer.prototype.createAudioNode = function (containerID, effectID) {
- console.log("creating "+ effectID +" AudioNode with containerID " + containerID);
- switch (effectID) {
- case "delay":
- this.audioNodes[containerID] = CONTEXT.createDelay();
- break;
- case "eq":
- this.audioNodes[containerID] = CONTEXT.createBiquadFilter();
- break;
- case "pan":
- this.audioNodes[containerID] = CONTEXT.createPanner();
- break;
- case "compressor":
- this.audioNodes[containerID] = CONTEXT.createDynamicsCompressor();
- break;
- case "gain":
- this.audioNodes[containerID] = CONTEXT.createGain();
- break;
- case "clip":
- this.createFileSourceNode(containerID);
- break;
- default:
- console.error("Unknown effect " + effect_id);
- break;
- }
- };
- // apply parameters to an AudiNode
- // NOTE gain changes for file sources and hidden gain containers need the right containerID of the actual gain container
- EffectChainMixer.prototype.applyParameters = function(containerID, parameters) {
- console.log("applying parameters");
- // special threatment for panner nodes
- if (typeof this.audioNodes[containerID] == "PannerNode") {
- var pos;
- this.audioNodes[containerID].panningModel =params.model;
- if (parameters.type == "2D") {
- pos = panPos(parameters.value);
- }
- else {
- pos = parameters.pos;
- }
- this.audioNodes[containerID].setPosition(pos.x, pos.y, pos.z);
- }
- else {
- for (param in parameters) {
- // special threament for mode changes
- if (param == "mode") {
- // TODO: implement mode switches
- switch (parameters[param]) {
- case "normal":
- case "mute":
- case "solo":
- }
- }
- console.log("parameter: " + param + ", value: " + parameters[param].value);
- console.log("node " + this.audioNodes[containerID]);
- this.audioNodes[containerID][param] = parameters[param].value;
- }
- }
- };
- // creates an AudioBufferSourceNode after loading and decoding the audio file
- EffectChainMixer.prototype.createFileSourceNode = function(containerID) {
- // increase counter of files loading
- this.loadingFiles += 1;
- console.log("creating fileSourceNode");
- var that, url, sourceID, request, buffer;
- console.log("container "+containerID+": " + this.containerDict[containerID]);
- that = this;
- for (sourceID in this.containerDict[containerID].sources) {
- url = sourceID.slice(FILE_KEYWORD_LENGTH);
- }
- request = new XMLHttpRequest();
- request.onerror = function(error) { console.error('XMLHttpRequest error: ' + error) };
- request.open('GET', url, true);
- request.responseType = 'arraybuffer';
- // decode asynchronously on load
- request.onload = function() {
- CONTEXT.decodeAudioData(request.response, function(buffer) {
- if (!buffer) {
- console.error('error decoding file data: ' + url);
- return;
- }
- that.audioNodes[containerID] = CONTEXT.createBufferSource();
- that.audioNodes[containerID].buffer = buffer;
- that.audioNodes[containerID].mode = "normal";
- that.applyParameters(containerID, that.containerDict[containerID].effect.timepoints[0].change);
- length = that.audioNodes[containerID].buffer.duration+that.audioNodes[containerID].offset;
- if (length > that.times.songLength) {
- console.log("NEW SONGLENGTH: " + length);
- that.times.songLength = length;
- }
- console.log("finished loading " +url);
- // decrease counter of files loading
- that.loadingFiles -= 1;
- },
- function(error) {console.error('decodeAudioData error', error); });
- };
- request.send();
- };
- // connect all AudioNodes
- EffectChainMixer.prototype.connectAudioNodes = function() {
- console.log("connecting AudioNodes");
- // wait for all files to be loaded
- if (this.loadingFiles > 0) {
- var that = this;
- console.log("waiting for " + this.loadingFiles + " loading files..");
- setTimeout(function(){that.connectAudioNodes(); },FILE_LOAD_WAIT);
- }
- else{
- var containerID, container, consumerID, fileSourceID, sourceContainerID, hasConsumer;
- for (containerID in this.containerDict) {
- container = clone(this.containerDict[containerID]);
- sourceContainerID = containerID;
- hasConsumer = false;
- // check if source of the container is a file and connect it to its gain container
- if (containerID in this.fileSources) {
- for (fileSourceID in container.sources) {
- // the gain container is the new source for consumers
- sourceContainerID = fileSourceID+"to"+containerID;
- console.log("TYPEOF " + containerID + ": " + typeof(this.audioNodes[containerID]));
- this.audioNodes[containerID].connect(this.audioNodes[sourceContainerID]);
- }
- }
- // iterate trough all consumers of the container
- for (consumerID in container.consumers) {
- hasConsumer = true;
- // check for hidden gain container between this container and the consumer
- // connect the hidden gain container to the actuall container, then redirect
- // the output of the source to the hidden gain container
- if (this.containerDict[consumerID].sources[containerId].volume != 1) {
- this.audioNodes[containerID + "to" + consumerID].connect(consumerID);
- consumerID = containerID + "to" + consumerID;
- }
- this.audioNodes[sourceContainerID].connect(consumerID);
- }
- // if the container has no consumer, connect it to the master output
- if (!hasConsumer) {
- this.audioNodes[sourceContainerID].connect(this.audioNodes["MASTER_GAIN"]);
- }
- }
- }
- };
- // start playback
- EffectChainMixer.prototype.play = function() {
- if (!this.isPlaying) {
- // wait for all files to be loaded
- if (this.loadingFiles > 0) {
- var that = this;
- console.log("waiting for " + this.loadingFiles + " loading files..");
- setTimeout(function(){that.play();},FILE_LOAD_WAIT);
- }
- else {
- this.connectAudioNodes();
- this.isPlaying = true;
- var currentTime = CONTEXT.currentTime;
- this.times.contextTime = currentTime;
- for (sourceID in this.fileSources) {
- console.log("starting playback of "+sourceID);
- params = this.fileSources[sourceID].effect.timepoints[0].change;
- this.audioNodes[sourceID].start(params.offset.value + currentTime, params.trim.value + this.times.songPos, params.duration.value);
- }
- }
- }
- };
- // pause playback
- EffectChainMixer.prototype.pause = function() {
- if (this.isPlaying) {
- var now = CONTEXT.currentTime;
- this.stop();
- this.times.songPos = now - this.times.contextTime;
- }
- };
- // seek to certain point in track
- EffectChainMixer.prototype.seek = function(time) {
- this.stop();
- this.times.songPos = time;
- this.play();
- };
- // pause playback and reset to start of track
- EffectChainMixer.prototype.stop = function() {
- if (this.isPlaying) {
- for (sourceID in this.fileSources) {
- this.audioNodes[sourceID].noteOff(0);
- }
- this.isPlaying = false;
- }
- this.times.songPos = 0;
- };
- /////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // helper functions
- /////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // fastest possible object cloning,called "Cached Crockford"
- function F() {}
- function clone(o) {
- F.prototype = o;
- return new F();
- }
- // calculate position for 2D panning
- function panPos(val) {
- var xDeg = val;
- var zDeg = xDeg + 90;
- if (zDeg > 90) {
- zDeg = 180 - zDeg;
- }
- var x = Math.sin(xDeg * (Math.PI / 180));
- var z = Math.sin(zDeg * (Math.PI / 180));
- return {"x" : x , "y" : 0, "z" : z};
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement