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