|
|
- /**
- * Tutorials:
- * http://www.html5rocks.com/en/tutorials/webaudio/games/
- * http://www.html5rocks.com/en/tutorials/webaudio/positional_audio/ <- +1 as it is three.js
- * http://www.html5rocks.com/en/tutorials/webaudio/intro/
- *
- * Spec:
- * https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html
- *
- * Chromium Demo:
- * http://chromium.googlecode.com/svn/trunk/samples/audio/index.html <- running page
- * http://code.google.com/p/chromium/source/browse/trunk/samples/audio/ <- source
- */
-
-
- /**
- * Notes on removing tQuery dependancy
- * * some stuff depends on tQuery
- * * find which one
- * * tQuery.Webaudio got a world link for the listener
- * * do a plugin with followListener(world), unfollowListener(world)
- * * namespace become WebAudio.* instead of WebAudio.*
- */
-
- //////////////////////////////////////////////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////////
- // WebAudio //
- //////////////////////////////////////////////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////////
-
-
- window.AudioContext = window.AudioContext || window.webkitAudioContext;
-
- /**
- * Main class to handle webkit audio
- *
- * TODO make the clip detector from http://www.html5rocks.com/en/tutorials/webaudio/games/
- *
- * @class Handle webkit audio API
- *
- * @param {tQuery.World} [world] the world on which to run
- */
- WebAudio = function () {
- // sanity check - the api MUST be available
- if (WebAudio.isAvailable === false) {
- this._addRequiredMessage();
- // Throw an error to stop execution
- throw new Error('WebAudio API is required and not available.')
- }
-
- // create the context
- this._ctx = new AudioContext();
- // setup internal variable
- this._muted = false;
- this._volume = 1;
-
- // setup the end of the node chain
- // TODO later code the clipping detection from http://www.html5rocks.com/en/tutorials/webaudio/games/
- this._gainNode = this._ctx.createGain();
- this._compressor = this._ctx.createDynamicsCompressor();
- this._gainNode.connect(this._compressor);
- this._compressor.connect(this._ctx.destination);
-
- // init page visibility
- this._pageVisibilityCtor();
- };
-
-
- /**
- * vendor.js way to make plugins ala jQuery
- * @namespace
- */
- WebAudio.fn = WebAudio.prototype;
-
-
- /**
- * destructor
- */
- WebAudio.prototype.destroy = function () {
- this._pageVisibilityDtor();
- };
-
- /**
- * @return {Boolean} true if it is available or not
- */
- WebAudio.isAvailable = window.AudioContext ? true : false;
-
- //////////////////////////////////////////////////////////////////////////////////
- // comment //
- //////////////////////////////////////////////////////////////////////////////////
-
- WebAudio.prototype._addRequiredMessage = function (parent) {
- // handle defaults arguements
- parent = parent || document.body;
- // message directly taken from Detector.js
- var domElement = document.createElement('div');
- domElement.style.fontFamily = 'monospace';
- domElement.style.fontSize = '13px';
- domElement.style.textAlign = 'center';
- domElement.style.background = '#eee';
- domElement.style.color = '#000';
- domElement.style.padding = '1em';
- domElement.style.width = '475px';
- domElement.style.margin = '5em auto 0';
- domElement.innerHTML = [
- 'Your browser does not seem to support <a href="https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html">WebAudio API</a>.<br />',
- 'Try with <a href="https://www.google.com/intl/en/chrome/browser/">Chrome Browser</a>.'
- ].join('\n');
- // add it to the parent
- parent.appendChild(domElement);
- }
-
- //////////////////////////////////////////////////////////////////////////////////
- // //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * get the audio context
- *
- * @returns {AudioContext} the audio context
- */
- WebAudio.prototype.context = function () {
- return this._ctx;
- };
-
- /**
- * Create a sound
- *
- * @returns {WebAudio.Sound} the sound just created
- */
- WebAudio.prototype.createSound = function () {
- var webaudio = this;
- var sound = new WebAudio.Sound(webaudio);
- return sound;
- }
-
-
- /**
- * return the entry node in the master node chains
- */
- WebAudio.prototype._entryNode = function () {
- //return this._ctx.destination;
- return this._gainNode;
- }
-
- //////////////////////////////////////////////////////////////////////////////////
- // volume/mute //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * getter/setter on the volume
- */
- WebAudio.prototype.volume = function (value) {
- if (value === undefined) return this._volume;
- // update volume
- this._volume = value;
- // update actual volume IIF not muted
- if (this._muted === false) {
- this._gainNode.gain.value = this._volume;
- }
- // return this for chained API
- return this;
- };
-
- /**
- * getter/setter for mute
- */
- WebAudio.prototype.mute = function (value) {
- if (value === undefined) return this._muted;
- this._muted = value;
- this._gainNode.gain.value = this._muted ? 0 : this._volume;
- return this; // for chained API
- }
-
- /**
- * to toggle the mute
- */
- WebAudio.prototype.toggleMute = function () {
- if (this.mute()) this.mute(false);
- else this.mute(true);
- }
-
- //////////////////////////////////////////////////////////////////////////////////
- // pageVisibility //
- //////////////////////////////////////////////////////////////////////////////////
-
-
- WebAudio.prototype._pageVisibilityCtor = function () {
- // shim to handle browser vendor
- this._pageVisibilityEventStr = (document.hidden !== undefined ? 'visibilitychange' :
- (document.mozHidden !== undefined ? 'mozvisibilitychange' :
- (document.msHidden !== undefined ? 'msvisibilitychange' :
- (document.webkitHidden !== undefined ? 'webkitvisibilitychange' :
- console.assert(false, "Page Visibility API unsupported")
- ))));
- this._pageVisibilityDocumentStr = (document.hidden !== undefined ? 'hidden' :
- (document.mozHidden !== undefined ? 'mozHidden' :
- (document.msHidden !== undefined ? 'msHidden' :
- (document.webkitHidden !== undefined ? 'webkitHidden' :
- console.assert(false, "Page Visibility API unsupported")
- ))));
- // event handler for visibilitychange event
- this._$pageVisibilityCallback = function () {
- var isHidden = document[this._pageVisibilityDocumentStr] ? true : false;
- this.mute(isHidden ? true : false);
- }.bind(this);
- // bind the event itself
- document.addEventListener(this._pageVisibilityEventStr, this._$pageVisibilityCallback, false);
- }
-
- WebAudio.prototype._pageVisibilityDtor = function () {
- // unbind the event itself
- document.removeEventListener(this._pageVisibilityEventStr, this._$pageVisibilityCallback, false);
- }
- /**
- * Constructor
- *
- * @class builder to generate nodes chains. Used in WebAudio.Sound
- * @param {AudioContext} audioContext the audio context
- */
- WebAudio.NodeChainBuilder = function (audioContext) {
- console.assert(audioContext instanceof AudioContext);
- this._context = audioContext;
- this._firstNode = null;
- this._lastNode = null;
- this._nodes = {};
- };
-
- /**
- * creator
- *
- * @param {webkitAudioContext} audioContext the context
- * @return {WebAudio.NodeChainBuider} just created object
- */
- WebAudio.NodeChainBuilder.create = function (audioContext) {
- return new WebAudio.NodeChainBuilder(audioContext);
- }
-
- /**
- * destructor
- */
- WebAudio.NodeChainBuilder.prototype.destroy = function () {
- };
-
- //////////////////////////////////////////////////////////////////////////////////
- // getters //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * getter for the nodes
- */
- WebAudio.NodeChainBuilder.prototype.nodes = function () {
- return this._nodes;
- }
-
- /**
- * @returns the first node of the chain
- */
- WebAudio.NodeChainBuilder.prototype.first = function () {
- return this._firstNode;
- }
-
- /**
- * @returns the last node of the chain
- */
- WebAudio.NodeChainBuilder.prototype.last = function () {
- return this._lastNode;
- }
-
- //////////////////////////////////////////////////////////////////////////////////
- // //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * add a node to the chain
- * @param {[type]} node [description]
- * @param {[type]} properties [description]
- */
- WebAudio.NodeChainBuilder.prototype._addNode = function (node, properties) {
- // update this._bufferSourceDst - needed for .cloneBufferSource()
- var lastIsBufferSource = this._lastNode && ('playbackRate' in this._lastNode) ? true : false;
- if (lastIsBufferSource) this._bufferSourceDst = node;
-
- // connect this._lastNode to node if suitable
- if (this._lastNode !== null) this._lastNode.connect(node);
-
- // update this._firstNode && this._lastNode
- if (this._firstNode === null) this._firstNode = node;
- this._lastNode = node;
-
- // apply properties to the node
- for (var property in properties) {
- node[property] = properties[property];
- }
-
- // for chained API
- return this;
- };
-
-
- //////////////////////////////////////////////////////////////////////////////////
- // creator for each type of nodes //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * Clone the bufferSource. Used just before playing a sound
- * @returns {AudioBufferSourceNode} the clone AudioBufferSourceNode
- */
- WebAudio.NodeChainBuilder.prototype.cloneBufferSource = function () {
- console.assert(this._nodes.bufferSource, "no buffersource presents. Add one.");
- var orig = this._nodes.bufferSource;
- var clone = this._context.createBufferSource()
- clone.buffer = orig.buffer;
- clone.playbackRate = orig.playbackRate;
- clone.loop = orig.loop;
- clone.connect(this._bufferSourceDst);
- return clone;
- }
-
- /**
- * add a bufferSource
- *
- * @param {Object} [properties] properties to set in the created node
- */
- WebAudio.NodeChainBuilder.prototype.bufferSource = function (properties) {
- var node = this._context.createBufferSource()
- this._nodes.bufferSource = node;
- return this._addNode(node, properties)
- };
-
- /**
- * add a createMediaStreamSource
- *
- * @param {Object} [properties] properties to set in the created node
- */
- WebAudio.NodeChainBuilder.prototype.mediaStreamSource = function (stream, properties) {
- // console.assert( stream instanceof LocalMediaStream )
- var node = this._context.createMediaStreamSource(stream)
- this._nodes.bufferSource = node;
- return this._addNode(node, properties)
- };
-
- /**
- * add a createMediaElementSource
- * @param {HTMLElement} element the element to add
- * @param {Object} [properties] properties to set in the created node
- */
- WebAudio.NodeChainBuilder.prototype.mediaElementSource = function (element, properties) {
- console.assert(element instanceof HTMLAudioElement || element instanceof HTMLVideoElement)
- var node = this._context.createMediaElementSource(element)
- this._nodes.bufferSource = node;
- return this._addNode(node, properties)
- };
-
- /**
- * add a panner
- *
- * @param {Object} [properties] properties to set in the created node
- */
- WebAudio.NodeChainBuilder.prototype.panner = function (properties) {
- var node = this._context.createPanner()
- this._nodes.panner = node;
- return this._addNode(node, properties)
- };
-
- /**
- * add a analyser
- *
- * @param {Object} [properties] properties to set in the created node
- */
- WebAudio.NodeChainBuilder.prototype.analyser = function (properties) {
- var node = this._context.createAnalyser()
- this._nodes.analyser = node;
- return this._addNode(node, properties)
- };
-
- /**
- * add a gainNode
- *
- * @param {Object} [properties] properties to set in the created node
- */
- WebAudio.NodeChainBuilder.prototype.gainNode = function (properties) {
- var node = this._context.createGain()
- this._nodes.gainNode = node;
- return this._addNode(node, properties)
- };
-
- /**
- * sound instance
- *
- * @class Handle one sound for WebAudio
- *
- * @param {tQuery.World} [world] the world on which to run
- * @param {WebAudio.NodeChainBuilder} [nodeChain] the nodeChain to use
- */
- WebAudio.Sound = function (webaudio, nodeChain) {
- this._webaudio = webaudio;
- this._context = this._webaudio.context();
-
- console.assert(this._webaudio instanceof WebAudio);
-
- // create a default NodeChainBuilder if needed
- if (nodeChain === undefined) {
- nodeChain = new WebAudio.NodeChainBuilder(this._context)
- .bufferSource().gainNode().analyser().panner();
- }
- // setup this._chain
- console.assert(nodeChain instanceof WebAudio.NodeChainBuilder);
- this._chain = nodeChain;
- // connect this._chain.last() node to this._webaudio._entryNode()
- this._chain.last().connect(this._webaudio._entryNode());
-
- // create some alias
- this._source = this._chain.nodes().bufferSource;
- this._gainNode = this._chain.nodes().gainNode;
- this._analyser = this._chain.nodes().analyser;
- this._panner = this._chain.nodes().panner;
-
- // sanity check
- console.assert(this._source, "no bufferSource: not yet supported")
- console.assert(this._gainNode, "no gainNode: not yet supported")
- console.assert(this._analyser, "no analyser: not yet supported")
- console.assert(this._panner, "no panner: not yet supported")
- };
-
- WebAudio.Sound.create = function (webaudio, nodeChain) {
- return new WebAudio.Sound(webaudio, nodeChain);
- }
-
- /**
- * destructor
- */
- WebAudio.Sound.prototype.destroy = function () {
- // disconnect from this._webaudio
- this._chain.last().disconnect();
- // destroy this._chain
- this._chain.destroy();
- this._chain = null;
- };
-
- /**
- * vendor.js way to make plugins ala jQuery
- * @namespace
- */
- WebAudio.Sound.fn = WebAudio.Sound.prototype;
-
- //////////////////////////////////////////////////////////////////////////////////
- // //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * getter of the chain nodes
- */
- WebAudio.Sound.prototype.nodes = function () {
- return this._chain.nodes();
- };
-
- /**
- * @returns {Boolean} true if the sound is playable, false otherwise
- */
- WebAudio.Sound.prototype.isPlayable = function () {
- return this._source.buffer ? true : false;
- };
-
- /**
- * play the sound
- *
- * @param {Number} [time] time when to play the sound
- */
- WebAudio.Sound.prototype.play = function (time) {
- // handle parameter polymorphism
- if (time === undefined) time = 0;
- // if not yet playable, ignore
- // - usefull when the sound download isnt yet completed
- if (this.isPlayable() === false) return;
- // clone the bufferSource
- var clonedNode = this._chain.cloneBufferSource();
- // set the noteOn
- clonedNode.start(time);
- // create the source object
- var source = {
- node: clonedNode,
- stop: function (time) {
- if (time === undefined) time = 0;
- this.node.stop(time);
- return source; // for chained API
- }
- }
- // return it
- return source;
- };
-
- /**
- * getter/setter on the volume
- *
- * @param {Number} [value] the value to set, if not provided, get current value
- */
- WebAudio.Sound.prototype.volume = function (value) {
- if (value === undefined) return this._gainNode.gain.value;
- this._gainNode.gain.value = value;
- return this; // for chained API
- };
-
-
- /**
- * getter/setter on the loop
- *
- * @param {Number} [value] the value to set, if not provided, get current value
- */
- WebAudio.Sound.prototype.loop = function (value) {
- if (value === undefined) return this._source.loop;
- this._source.loop = value;
- return this; // for chained API
- };
-
- /**
- * getter/setter on the source buffer
- *
- * @param {Number} [value] the value to set, if not provided, get current value
- */
- WebAudio.Sound.prototype.buffer = function (value) {
- if (value === undefined) return this._source.buffer;
- this._source.buffer = value;
- return this; // for chained API
- };
-
-
- /**
- * Set parameter for the pannerCone
- *
- * @param {Number} innerAngle the inner cone hangle in radian
- * @param {Number} outerAngle the outer cone hangle in radian
- * @param {Number} outerGain the gain to apply when in the outerCone
- */
- WebAudio.Sound.prototype.pannerCone = function (innerAngle, outerAngle, outerGain) {
- this._panner.coneInnerAngle = innerAngle * 180 / Math.PI;
- this._panner.coneOuterAngle = outerAngle * 180 / Math.PI;
- this._panner.coneOuterGain = outerGain;
- return this; // for chained API
- };
-
- /**
- * getter/setter on the pannerConeInnerAngle
- *
- * @param {Number} value the angle in radian
- */
- WebAudio.Sound.prototype.pannerConeInnerAngle = function (value) {
- if (value === undefined) return this._panner.coneInnerAngle / 180 * Math.PI;
- this._panner.coneInnerAngle = value * 180 / Math.PI;
- return this; // for chained API
- };
-
- /**
- * getter/setter on the pannerConeOuterAngle
- *
- * @param {Number} value the angle in radian
- */
- WebAudio.Sound.prototype.pannerConeOuterAngle = function (value) {
- if (value === undefined) return this._panner.coneOuterAngle / 180 * Math.PI;
- this._panner.coneOuterAngle = value * 180 / Math.PI;
- return this; // for chained API
- };
-
- /**
- * getter/setter on the pannerConeOuterGain
- *
- * @param {Number} value the value
- */
- WebAudio.Sound.prototype.pannerConeOuterGain = function (value) {
- if (value === undefined) return this._panner.coneOuterGain;
- this._panner.coneOuterGain = value;
- return this; // for chained API
- };
-
- /**
- * compute the amplitude of the sound (not sure at all it is the proper term)
- *
- * @param {Number} width the number of frequencyBin to take into account
- * @returns {Number} return the amplitude of the sound
- */
- WebAudio.Sound.prototype.amplitude = function (width) {
- // handle paramerter
- width = width !== undefined ? width : 2;
- // inint variable
- var analyser = this._analyser;
- var freqByte = new Uint8Array(analyser.frequencyBinCount);
- // get the frequency data
- analyser.getByteFrequencyData(freqByte);
- // compute the sum
- var sum = 0;
- for (var i = 0; i < width; i++) {
- sum += freqByte[i];
- }
- // complute the amplitude
- var amplitude = sum / (width * 256 - 1);
- // return ampliture
- return amplitude;
- }
-
- /**
- * Generate a sinusoid buffer.
- * FIXME should likely be in a plugin
- */
- WebAudio.Sound.prototype.tone = function (hertz, seconds) {
- // handle parameter
- hertz = hertz !== undefined ? hertz : 200;
- seconds = seconds !== undefined ? seconds : 1;
- // set default value
- var nChannels = 1;
- var sampleRate = 44100;
- var amplitude = 2;
- // create the buffer
- var buffer = this._webaudio.context().createBuffer(nChannels, seconds * sampleRate, sampleRate);
- var fArray = buffer.getChannelData(0);
- // fill the buffer
- for (var i = 0; i < fArray.length; i++) {
- var time = i / buffer.sampleRate;
- var angle = hertz * time * Math.PI;
- fArray[i] = Math.sin(angle) * amplitude;
- }
- // set the buffer
- this.buffer(buffer);
- return this; // for chained API
- }
-
-
- /**
- * Put this function is .Sound with getByt as private callback
- */
- WebAudio.Sound.prototype.makeHistogram = function (nBar) {
- // get analyser node
- var analyser = this._analyser;
- // allocate the private histo if needed - to avoid allocating at every frame
- //this._privHisto = this._privHisto || new Float32Array(analyser.frequencyBinCount);
- this._privHisto = this._privHisto || new Uint8Array(analyser.frequencyBinCount);
- // just an alias
- var freqData = this._privHisto;
-
- // get the data
- //analyser.getFloatFrequencyData(freqData);
- analyser.getByteFrequencyData(freqData);
- //analyser.getByteTimeDomainData(freqData);
-
- /**
- * This should be in imageprocessing.js almost
- */
- var makeHisto = function (srcArr, dstLength) {
- var barW = Math.floor(srcArr.length / dstLength);
- var nBar = Math.floor(srcArr.length / barW);
- var arr = []
- for (var x = 0, arrIdx = 0; x < srcArr.length; arrIdx++) {
- var sum = 0;
- for (var i = 0; i < barW; i++, x++) {
- sum += srcArr[x];
- }
- var average = sum / barW;
- arr[arrIdx] = average;
- }
- return arr;
- }
- // build the histo
- var histo = makeHisto(freqData, nBar);
- // return it
- return histo;
- }
-
- //////////////////////////////////////////////////////////////////////////////////
- // //
- //////////////////////////////////////////////////////////////////////////////////
-
- /**
- * Load a sound
- *
- * @param {String} url the url of the sound to load
- * @param {Function} onSuccess function to notify once the url is loaded (optional)
- * @param {Function} onError function to notify if an error occurs (optional)
- */
- WebAudio.Sound.prototype.load = function (url, onSuccess, onError) {
- // handle default arguments
- onError = onError || function () {
- console.warn("unable to load sound " + url);
- };
- // try to load the user
- this._loadAndDecodeSound(url, function (buffer) {
- this._source.buffer = buffer;
- onSuccess && onSuccess(this);
- }.bind(this), function () {
- onError && onError(this);
- }.bind(this));
- return this; // for chained API
- };
-
- /**
- * Load and decode a sound
- *
- * @param {String} url the url where to get the sound
- * @param {Function} onLoad the function called when the sound is loaded and decoded (optional)
- * @param {Function} onError the function called when an error occured (optional)
- */
- WebAudio.Sound.prototype._loadAndDecodeSound = function (url, onLoad, onError) {
- var context = this._context;
-
- context.decodeAudioData(url, function (buffer) {
- console.log("OK")
- onLoad && onLoad(buffer);
- }, function () {
- console.log("KO")
- onError && onError();
- });
- }
- /**
- * gowiththeflow.js - a javascript flow control micro library
- * https://github.com/jeromeetienne/gowiththeflow.js
- */
- WebAudio.Flow = function () {
- var self, stack = [], timerId = setTimeout(function () {
- timerId = null;
- self._next();
- }, 0);
- return self = {
- destroy: function () {
- timerId && clearTimeout(timerId);
- },
- par: function (callback, isSeq) {
- if (isSeq || !(stack[stack.length - 1] instanceof Array)) stack.push([]);
- stack[stack.length - 1].push(callback);
- return self;
- }, seq: function (callback) {
- return self.par(callback, true);
- },
- _next: function (err, result) {
- var errors = [], results = [], callbacks = stack.shift() || [], nbReturn = callbacks.length, isSeq = nbReturn == 1;
- for (var i = 0; i < callbacks.length; i++) {
- (function (fct, index) {
- fct(function (error, result) {
- errors[index] = error;
- results[index] = result;
- if (--nbReturn == 0) self._next(isSeq ? errors[0] : errors, isSeq ? results[0] : results)
- }, err, result)
- })(callbacks[i], i);
- }
- }
- }
- };
|