1 /**
  2  * sound instance
  3  *
  4  * @class Handle one sound for WebAudio
  5  *
  6  * @param {tQuery.World} [world] the world on which to run
  7  * @param {WebAudio.NodeChainBuilder} [nodeChain] the nodeChain to use
  8 */
  9 WebAudio.Sound	= function(webaudio, nodeChain){
 10 	this._webaudio	= webaudio;
 11 	this._context	= this._webaudio.context();
 12 
 13 	console.assert( this._webaudio instanceof WebAudio );
 14 
 15 	// create a default NodeChainBuilder if needed
 16 	if( nodeChain === undefined ){
 17 		nodeChain	= new WebAudio.NodeChainBuilder(this._context)
 18 					.bufferSource().gainNode().analyser().panner();
 19 	}
 20 	// setup this._chain
 21 	console.assert( nodeChain instanceof WebAudio.NodeChainBuilder );
 22 	this._chain	= nodeChain;
 23 
 24 	// connect this._chain.last() node to this._webaudio._entryNode()
 25 	this._chain.last().connect( this._webaudio._entryNode() );
 26 	
 27 	// create some alias
 28 	this._source	= this._chain.nodes().bufferSource;
 29 	this._gainNode	= this._chain.nodes().gainNode;
 30 	this._analyser	= this._chain.nodes().analyser;
 31 	this._panner	= this._chain.nodes().panner;
 32 	
 33 	// sanity check
 34 	console.assert(this._source	, "no bufferSource: not yet supported")
 35 	console.assert(this._gainNode	, "no gainNode: not yet supported")
 36 	console.assert(this._analyser	, "no analyser: not yet supported")
 37 	console.assert(this._panner	, "no panner: not yet supported")
 38 };
 39 
 40 /**
 41  * destructor
 42 */
 43 WebAudio.Sound.prototype.destroy	= function(){
 44 	// disconnect from this._webaudio
 45 	this._chain.last().disconnect();
 46 	// destroy this._chain
 47 	this._chain.destroy();
 48 	this._chain	= null;
 49 };
 50 
 51 /**
 52  * vendor.js way to make plugins ala jQuery
 53  * @namespace
 54 */
 55 WebAudio.Sound.fn	= WebAudio.Sound.prototype;
 56 
 57 //////////////////////////////////////////////////////////////////////////////////
 58 //										//
 59 //////////////////////////////////////////////////////////////////////////////////
 60 
 61 /**
 62  * getter of the chain nodes
 63 */
 64 WebAudio.Sound.prototype.nodes	= function(){
 65 	return this._chain.nodes();
 66 };
 67 
 68 /**
 69  * @returns {Boolean} true if the sound is playable, false otherwise
 70 */
 71 WebAudio.Sound.prototype.isPlayable	= function(){
 72 	return this._source.buffer ? true : false;
 73 };
 74 
 75 /**
 76  * play the sound
 77  *
 78  * @param {Number} [time] time when to play the sound
 79 */
 80 WebAudio.Sound.prototype.play		= function(time){
 81 	if( time ===  undefined )	time	= 0;
 82 	// clone the bufferSource
 83 	var clonedNode	= this._chain.cloneBufferSource();
 84 	// set the noteOn
 85 	clonedNode.noteOn(time);
 86 	// create the source object
 87 	var source	= {
 88 		node	: clonedNode,
 89 		stop	: function(time){
 90 			if( time ===  undefined )	time	= 0;
 91 			this.node.noteOff(time);
 92 			return source;	// for chained API
 93 		}
 94 	}
 95 	// return it
 96 	return source;
 97 };
 98 
 99 /**
100  * getter/setter on the volume
101  *
102  * @param {Number} [value] the value to set, if not provided, get current value
103 */
104 WebAudio.Sound.prototype.volume	= function(value){
105 	if( value === undefined )	return this._gainNode.gain.value;
106 	this._gainNode.gain.value	= value;
107 	return this;	// for chained API
108 };
109 
110 
111 /**
112  * getter/setter on the loop
113  * 
114  * @param {Number} [value] the value to set, if not provided, get current value
115 */
116 WebAudio.Sound.prototype.loop	= function(value){
117 	if( value === undefined )	return this._source.loop;
118 	this._source.loop	= value;
119 	return this;	// for chained API
120 };
121 
122 /**
123  * getter/setter on the source buffer
124  * 
125  * @param {Number} [value] the value to set, if not provided, get current value
126 */
127 WebAudio.Sound.prototype.buffer	= function(value){
128 	if( value === undefined )	return this._source.buffer;
129 	this._source.buffer	= value;
130 	return this;	// for chained API
131 };
132 
133 
134 /**
135  * Set parameter for the pannerCone
136  *
137  * @param {Number} innerAngle the inner cone hangle in radian
138  * @param {Number} outerAngle the outer cone hangle in radian
139  * @param {Number} outerGain the gain to apply when in the outerCone
140 */
141 WebAudio.Sound.prototype.pannerCone	= function(innerAngle, outerAngle, outerGain)
142 {
143 	this._panner.coneInnerAngle	= innerAngle * 180 / Math.PI;
144 	this._panner.coneOuterAngle	= outerAngle * 180 / Math.PI;
145 	this._panner.coneOuterGain	= outerGain;
146 	return this;	// for chained API
147 };
148 
149 /**
150  * getter/setter on the pannerConeInnerAngle
151  * 
152  * @param {Number} value the angle in radian
153 */
154 WebAudio.Sound.prototype.pannerConeInnerAngle	= function(value){
155 	if( value === undefined )	return this._panner.coneInnerAngle / 180 * Math.PI;
156 	this._panner.coneInnerAngle	= value * 180 / Math.PI;
157 	return this;	// for chained API
158 };
159 
160 /**
161  * getter/setter on the pannerConeOuterAngle
162  *
163  * @param {Number} value the angle in radian
164 */
165 WebAudio.Sound.prototype.pannerConeOuterAngle	= function(value){
166 	if( value === undefined )	return this._panner.coneOuterAngle / 180 * Math.PI;
167 	this._panner.coneOuterAngle	= value * 180 / Math.PI;
168 	return this;	// for chained API
169 };
170 
171 /**
172  * getter/setter on the pannerConeOuterGain
173  * 
174  * @param {Number} value the value
175 */
176 WebAudio.Sound.prototype.pannerConeOuterGain	= function(value){
177 	if( value === undefined )	return this._panner.coneOuterGain;
178 	this._panner.coneOuterGain	= value;
179 	return this;	// for chained API
180 };
181 
182 /**
183  * compute the amplitude of the sound (not sure at all it is the proper term)
184  *
185  * @param {Number} width the number of frequencyBin to take into account
186  * @returns {Number} return the amplitude of the sound
187 */
188 WebAudio.Sound.prototype.amplitude	= function(width)
189 {
190 	// handle paramerter
191 	width		= width !== undefined ? width : 2;
192 	// inint variable
193 	var analyser	= this._analyser;
194 	var freqByte	= new Uint8Array(analyser.frequencyBinCount);
195 	// get the frequency data
196 	analyser.getByteFrequencyData(freqByte);
197 	// compute the sum
198 	var sum	= 0;
199 	for(var i = 0; i < width; i++){
200 		sum	+= freqByte[i];
201 	}
202 	// complute the amplitude
203 	var amplitude	= sum / (width*256-1);
204 	// return ampliture
205 	return amplitude;
206 }
207 
208 /**
209  * Generate a sinusoid buffer.
210  * FIXME should likely be in a plugin
211 */
212 WebAudio.Sound.prototype.tone	= function(hertz, seconds){
213 	// handle parameter
214 	hertz	= hertz !== undefined ? hertz : 200;
215 	seconds	= seconds !== undefined ? seconds : 1;
216 	// set default value	
217 	var nChannels	= 1;
218 	var sampleRate	= 44100;
219 	var amplitude	= 2;
220 	// create the buffer
221 	var buffer	= webaudio.context().createBuffer(nChannels, seconds*sampleRate, sampleRate);
222 	var fArray	= buffer.getChannelData(0);
223 	// filli the buffer
224 	for(var i = 0; i < fArray.length; i++){
225 		var time	= i / buffer.sampleRate;
226 		var angle	= hertz * time * Math.PI;
227 		fArray[i]	= Math.sin(angle)*amplitude;
228 	}
229 	// set the buffer
230 	this.buffer(buffer).loop(true);
231 	return this;	// for chained API
232 }
233 
234 
235 /**
236  * Put this function is .Sound with getByt as private callback
237 */
238 WebAudio.Sound.prototype.makeHistogram	= function(nBar)
239 {	
240 	// get analyser node
241 	var analyser	= this._analyser;
242 	// allocate the private histo if needed - to avoid allocating at every frame
243 	//this._privHisto	= this._privHisto || new Float32Array(analyser.frequencyBinCount);
244 	this._privHisto	= this._privHisto || new Uint8Array(analyser.frequencyBinCount);
245 	// just an alias
246 	var freqData	= this._privHisto;
247 
248 	// get the data
249 	//analyser.getFloatFrequencyData(freqData)
250 	analyser.getByteFrequencyData(freqData);
251 	//analyser.getByteTimeDomainData(freqData)
252 
253 	/**
254 	 * This should be in imageprocessing.js almost
255 	*/
256 	var makeHisto	= function(srcArr, dstLength){
257 		var barW	= Math.floor(srcArr.length / dstLength);
258 		var nBar	= Math.floor(srcArr.length / barW);
259 		var arr		= []
260 		for(var x = 0, arrIdx = 0; x < srcArr.length; arrIdx++){
261 			var sum	= 0;
262 			for(var i = 0; i < barW; i++, x++){
263 				sum += srcArr[x];
264 			}
265 			var average	= sum/barW;
266 			arr[arrIdx]	= average;
267 		}
268 		return arr;
269 	}
270 	// build the histo
271 	var histo	= makeHisto(freqData, nBar);
272 	// return it
273 	return histo;
274 }
275 
276 //////////////////////////////////////////////////////////////////////////////////
277 //										//
278 //////////////////////////////////////////////////////////////////////////////////
279 
280 /**
281  * Load a sound
282  *
283  * @param {String} url the url of the sound to load
284  * @param {Function} callback function to notify once the url is loaded (optional)
285 */
286 WebAudio.Sound.prototype.load = function(url, callback){
287 	this._loadAndDecodeSound(url, function(buffer){
288 		this._source.buffer	= buffer;
289 		callback && callback(this);
290 	}.bind(this), function(){
291 		console.warn("unable to load sound "+url);
292 	});
293 	return this;	// for chained API
294 };
295 
296 /**
297  * Load and decode a sound
298  *
299  * @param {String} url the url where to get the sound
300  * @param {Function} onLoad the function called when the sound is loaded and decoded (optional)
301  * @param {Function} onError the function called when an error occured (optional)
302 */
303 WebAudio.Sound.prototype._loadAndDecodeSound	= function(url, onLoad, onError){
304 	var context	= this._context;
305 	var request	= new XMLHttpRequest();
306 	request.open('GET', url, true);
307 	request.responseType	= 'arraybuffer';
308 	// Decode asynchronously
309 	request.onload	= function() {
310 		context.decodeAudioData(request.response, function(buffer) {
311 			onLoad && onLoad(buffer);
312 		}, function(){
313 			onError && onError();
314 		});
315 	};
316 	// actually start the request
317 	request.send();
318 }
319