1 /** 2 * Tutorials: 3 * http://www.html5rocks.com/en/tutorials/webaudio/games/ 4 * http://www.html5rocks.com/en/tutorials/webaudio/positional_audio/ <- +1 as it is three.js 5 * http://www.html5rocks.com/en/tutorials/webaudio/intro/ 6 * 7 * Spec: 8 * https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html 9 * 10 * Chromium Demo: 11 * http://chromium.googlecode.com/svn/trunk/samples/audio/index.html <- running page 12 * http://code.google.com/p/chromium/source/browse/trunk/samples/audio/ <- source 13 */ 14 15 16 17 18 ////////////////////////////////////////////////////////////////////////////////// 19 ////////////////////////////////////////////////////////////////////////////////// 20 ////////////////////////////////////////////////////////////////////////////////// 21 // tQuery.World.* WebAudio // 22 ////////////////////////////////////////////////////////////////////////////////// 23 ////////////////////////////////////////////////////////////////////////////////// 24 ////////////////////////////////////////////////////////////////////////////////// 25 26 27 28 tQuery.World.register('enableWebAudio', function(){ 29 // sanity check 30 console.assert( this.hasWebAudio() === false, "there is already a webaudio" ); 31 // intenciate a tQuery.World.WebAudio 32 var webaudio = new tQuery.WebAudio(this); 33 // store webaudio in the world 34 tQuery.data(this, "webaudio", webaudio); 35 // for chained API 36 return this; 37 }); 38 39 tQuery.World.register('disabledWebAudio', function(){ 40 if( this.hasWebAudio() === false ) return this; 41 var webaudio = tQuery.data(this, "webaudio"); 42 webaudio.destroy(); 43 tQuery.removeData(this, "webaudio"); 44 return this; // for chained API 45 }); 46 47 tQuery.World.register('getWebAudio', function(){ 48 var webaudio = tQuery.data(this, "webaudio"); 49 return webaudio; 50 }); 51 52 tQuery.World.register('hasWebAudio', function(){ 53 var webaudio = tQuery.data(this, "webaudio"); 54 return webaudio ? true : false; 55 }); 56 57 tQuery.World.register('supportWebAudio', function(){ 58 return tQuery.WebAudio.isAvailable; 59 }); 60 61 62 ////////////////////////////////////////////////////////////////////////////////// 63 ////////////////////////////////////////////////////////////////////////////////// 64 ////////////////////////////////////////////////////////////////////////////////// 65 // tQuery.WebAudio // 66 ////////////////////////////////////////////////////////////////////////////////// 67 ////////////////////////////////////////////////////////////////////////////////// 68 ////////////////////////////////////////////////////////////////////////////////// 69 70 71 /** 72 * Main class to handle webkit audio 73 * 74 * TODO make the clip detector from http://www.html5rocks.com/en/tutorials/webaudio/games/ 75 * 76 * @class Handle webkit audio API 77 * 78 * @param {tQuery.World} [world] the world on which to run 79 */ 80 tQuery.WebAudio = function(world){ 81 // sanity check - the api MUST be available 82 console.assert(tQuery.WebAudio.isAvailable === true, 'webkitAudioContext isnt available on your browser'); 83 84 // handle parameter 85 this._world = world ? world : tQuery.world; 86 87 // hook this._updateCb to this.world.loop() 88 this._updateCb = function(deltaTime){ 89 this.updateListener(this._world.camera(), deltaTime) 90 }.bind(this); 91 this._world.loop().hook(this._updateCb); 92 93 // create the context 94 this._ctx = new webkitAudioContext(); 95 96 // setup the end of the node chain 97 // TODO later code the clipping detection from http://www.html5rocks.com/en/tutorials/webaudio/games/ 98 this._gainNode = this._ctx.createGainNode(); 99 this._compressor= this._ctx.createDynamicsCompressor(); 100 this._gainNode.connect( this._compressor ); 101 this._compressor.connect( this._ctx.destination ); 102 }; 103 104 tQuery.WebAudio.prototype.destroy = function(){ 105 // unhook this._updateCb from this.world.loop() 106 this._world.loop().unhook(this._updateCb); 107 this._updateCb = null; 108 }; 109 110 /** 111 * 112 * 113 * @return {Boolean} true if it is available or not 114 */ 115 tQuery.WebAudio.isAvailable = window.webkitAudioContext ? true : false; 116 117 ////////////////////////////////////////////////////////////////////////////////// 118 // // 119 ////////////////////////////////////////////////////////////////////////////////// 120 121 /** 122 * get the audio context 123 * 124 * @returns {AudioContext} the audio context 125 */ 126 tQuery.WebAudio.prototype.context = function(){ 127 return this._ctx; 128 }; 129 130 /** 131 * return the entry node in the master node chains 132 */ 133 tQuery.WebAudio.prototype._entryNode = function(){ 134 //return this._ctx.destination; 135 return this._gainNode; 136 } 137 138 /** 139 * getter/setter on the volume 140 * 141 */ 142 tQuery.WebAudio.prototype.world = function(value){ 143 if( value === undefined ) return this._world; 144 this._world = value; 145 return this; 146 }; 147 148 /** 149 * getter/setter on the volume 150 */ 151 tQuery.WebAudio.prototype.volume = function(value){ 152 if( value === undefined ) return this._gainNode.gain.value; 153 this._gainNode.gain.value = value; 154 return this; 155 }; 156 157 tQuery.WebAudio.prototype.updateListener = function(object3d, deltaTime){ 158 var context = this._ctx; 159 // sanity check on parameters 160 console.assert( object3d instanceof THREE.Object3D ); 161 console.assert( typeof(deltaTime) === 'number' ); 162 163 // ensure object3d.matrixWorld is up to date 164 object3d.updateMatrixWorld(); 165 166 //////////////////////////////////////////////////////////////////////// 167 // set position 168 var position = object3d.matrixWorld.getPosition(); 169 context.listener.setPosition(position.x, position.y, position.z); 170 171 //////////////////////////////////////////////////////////////////////// 172 // set orientation 173 var mOrientation= object3d.matrixWorld.clone(); 174 // zero the translation 175 mOrientation.setPosition({x : 0, y: 0, z: 0}); 176 // Compute Front vector: Multiply the 0,0,1 vector by the world matrix and normalize the result. 177 var vFront= new THREE.Vector3(0,0,1); 178 mOrientation.multiplyVector3(vFront); 179 vFront.normalize(); 180 // Compute UP vector: Multiply the 0,-1,0 vector by the world matrix and normalize the result. 181 var vUp= new THREE.Vector3(0,-1, 0); 182 mOrientation.multiplyVector3(vUp); 183 vUp.normalize(); 184 // Set panner orientation 185 context.listener.setOrientation(vFront.x, vFront.y, vFront.z, vUp.x, vUp.y, vUp.z); 186 187 //////////////////////////////////////////////////////////////////////// 188 // set velocity 189 if( this._prevPos === undefined ){ 190 this._prevPos = object3d.matrixWorld.getPosition().clone(); 191 }else{ 192 var position = object3d.matrixWorld.getPosition(); 193 var velocity = position.clone().subSelf(this._prevPos).divideScalar(deltaTime); 194 this._prevPos = object3d.matrixWorld.getPosition().clone(); 195 context.listener.setVelocity(velocity.x, velocity.y, velocity.z); 196 } 197 } 198 199 200 201 ////////////////////////////////////////////////////////////////////////////////// 202 ////////////////////////////////////////////////////////////////////////////////// 203 ////////////////////////////////////////////////////////////////////////////////// 204 // tQuery.WebAudio.NodeChainBuilder // 205 ////////////////////////////////////////////////////////////////////////////////// 206 ////////////////////////////////////////////////////////////////////////////////// 207 ////////////////////////////////////////////////////////////////////////////////// 208 209 /** 210 * Constructor 211 * 212 * @class builder to generate nodes chains. Used in tQuery.WebAudio.Sound 213 * @param {webkitAudioContext} audioContext the audio context 214 */ 215 tQuery.WebAudio.NodeChainBuilder = function(audioContext){ 216 console.assert( audioContext instanceof webkitAudioContext ); 217 this._context = audioContext; 218 this._firstNode = null; 219 this._lastNode = null; 220 this._nodes = {}; 221 }; 222 223 /** 224 * destructor 225 */ 226 tQuery.WebAudio.NodeChainBuilder.prototype.destroy = function(){ 227 }; 228 229 /** 230 * getter for the nodes 231 */ 232 tQuery.WebAudio.NodeChainBuilder.prototype.nodes = function(){ 233 return this._nodes; 234 } 235 236 /** 237 * @returns the first node of the chain 238 */ 239 tQuery.WebAudio.NodeChainBuilder.prototype.first = function(){ 240 return this._firstNode; 241 } 242 243 /** 244 * @returns the last node of the chain 245 */ 246 tQuery.WebAudio.NodeChainBuilder.prototype.last = function(){ 247 return this._lastNode; 248 } 249 250 tQuery.WebAudio.NodeChainBuilder.prototype._addNode = function(node, properties) 251 { 252 // update this._bufferSourceDst - needed for .cloneBufferSource() 253 var lastIsBufferSource = this._lastNode && ('playbackRate' in this._lastNode) ? true : false; 254 if( lastIsBufferSource ) this._bufferSourceDst = node; 255 256 // connect this._lastNode to node if suitable 257 if( this._lastNode !== null ) this._lastNode.connect(node); 258 259 // update this._firstNode && this._lastNode 260 if( this._firstNode === null ) this._firstNode = node; 261 this._lastNode = node; 262 263 // apply properties to the node 264 for( var property in properties ){ 265 node[property] = properties[property]; 266 } 267 268 // for chained API 269 return this; 270 }; 271 272 273 /** 274 * Clone the bufferSource. Used just before playing a sound 275 * @returns {AudioBufferSourceNode} the clone AudioBufferSourceNode 276 */ 277 tQuery.WebAudio.NodeChainBuilder.prototype.cloneBufferSource = function(){ 278 console.assert(this._nodes.bufferSource, "no buffersource presents. Add one."); 279 var orig = this._nodes.bufferSource; 280 var clone = this._context.createBufferSource() 281 clone.buffer = orig.buffer; 282 clone.playbackRate = orig.playbackRate; 283 clone.loop = orig.loop; 284 clone.connect(this._bufferSourceDst); 285 return clone; 286 } 287 288 /** 289 * add a bufferSource 290 * 291 * @param {Object} [properties] properties to set in the created node 292 */ 293 tQuery.WebAudio.NodeChainBuilder.prototype.bufferSource = function(properties){ 294 var node = this._context.createBufferSource() 295 this._nodes.bufferSource= node; 296 return this._addNode(node, properties) 297 }; 298 299 /** 300 * add a panner 301 * 302 * @param {Object} [properties] properties to set in the created node 303 */ 304 tQuery.WebAudio.NodeChainBuilder.prototype.panner = function(properties){ 305 var node = this._context.createPanner() 306 this._nodes.panner = node; 307 return this._addNode(node, properties) 308 }; 309 310 /** 311 * add a analyser 312 * 313 * @param {Object} [properties] properties to set in the created node 314 */ 315 tQuery.WebAudio.NodeChainBuilder.prototype.analyser = function(properties){ 316 var node = this._context.createAnalyser() 317 this._nodes.analyser = node; 318 return this._addNode(node, properties) 319 }; 320 321 /** 322 * add a gainNode 323 * 324 * @param {Object} [properties] properties to set in the created node 325 */ 326 tQuery.WebAudio.NodeChainBuilder.prototype.gainNode = function(properties){ 327 var node = this._context.createGainNode() 328 this._nodes.gainNode = node; 329 return this._addNode(node, properties) 330 }; 331 332 333 ////////////////////////////////////////////////////////////////////////////////// 334 ////////////////////////////////////////////////////////////////////////////////// 335 ////////////////////////////////////////////////////////////////////////////////// 336 // tQuery.WebAudio.Sound // 337 ////////////////////////////////////////////////////////////////////////////////// 338 ////////////////////////////////////////////////////////////////////////////////// 339 ////////////////////////////////////////////////////////////////////////////////// 340 341 tQuery.register('createSound', function(world, nodeChain){ 342 return new tQuery.WebAudio.Sound(world, nodeChain); 343 }); 344 345 /** 346 * sound instance 347 * 348 * @class Handle one sound for tQuery.WebAudio 349 * 350 * @param {tQuery.World} [world] the world on which to run 351 * @param {tQuery.WebAudio.NodeChainBuilder} [nodeChain] the nodeChain to use 352 */ 353 tQuery.WebAudio.Sound = function(world, nodeChain){ 354 this._world = world ? world : tQuery.world; 355 this._webaudio = this._world.getWebAudio(); 356 this._context = this._webaudio.context(); 357 358 console.assert( this._world instanceof tQuery.World ); 359 360 // create a default NodeChainBuilder if needed 361 if( nodeChain === undefined ){ 362 nodeChain = new tQuery.WebAudio.NodeChainBuilder(this._context) 363 .bufferSource().gainNode().analyser().panner(); 364 } 365 // setup this._chain 366 console.assert( nodeChain instanceof tQuery.WebAudio.NodeChainBuilder ); 367 this._chain = nodeChain; 368 369 // connect this._chain.last() node to this._webaudio._entryNode() 370 this._chain.last().connect( this._webaudio._entryNode() ); 371 372 // create some alias 373 this._source = this._chain.nodes().bufferSource; 374 this._gainNode = this._chain.nodes().gainNode; 375 this._analyser = this._chain.nodes().analyser; 376 this._panner = this._chain.nodes().panner; 377 378 // sanity check 379 console.assert(this._source , "no bufferSource: not yet supported") 380 console.assert(this._gainNode , "no gainNode: not yet supported") 381 console.assert(this._analyser , "no analyser: not yet supported") 382 console.assert(this._panner , "no panner: not yet supported") 383 }; 384 385 /** 386 * destructor 387 */ 388 tQuery.WebAudio.Sound.prototype.destroy = function(){ 389 // disconnect from this._webaudio 390 this._chain.last().disconnect(); 391 // destroy this._chain 392 this._chain.destroy(); 393 this._chain = null; 394 }; 395 396 ////////////////////////////////////////////////////////////////////////////////// 397 // TODO put that in a plugins // 398 ////////////////////////////////////////////////////////////////////////////////// 399 400 /** 401 * follow a object3D 402 */ 403 tQuery.WebAudio.Sound.prototype.follow = function(object3d){ 404 console.assert( this.isFollowing() === false ); 405 // handle parameter 406 if( object3d instanceof tQuery.Object3D ){ 407 console.assert(object3d.length === 1) 408 object3d = object3d.get(0); 409 } 410 // sanity check on parameters 411 console.assert( object3d instanceof THREE.Object3D ); 412 413 // hook the world loop 414 this._followCb = function(deltaTime){ 415 this.updateWithObject3d(object3d, deltaTime); 416 }.bind(this); 417 this._world.loop().hook(this._followCb); 418 // for chained API 419 return this; 420 } 421 422 /** 423 * unfollow the object3D if any 424 */ 425 tQuery.WebAudio.Sound.prototype.unfollow = function(){ 426 this._world.loop().unhook(this._followCb); 427 this._followCb = null; 428 // for chained API 429 return this; 430 } 431 432 /** 433 * @returns {Boolean} true if this sound is following a object3d, false overwise 434 */ 435 tQuery.WebAudio.Sound.prototype.isFollowing = function(){ 436 return this._followCb ? true : false; 437 // for chained API 438 return this; 439 } 440 441 ////////////////////////////////////////////////////////////////////////////////// 442 // // 443 ////////////////////////////////////////////////////////////////////////////////// 444 445 /** 446 * getter of the chain nodes 447 */ 448 tQuery.WebAudio.Sound.prototype.nodes = function(){ 449 return this._chain.nodes(); 450 }; 451 452 /** 453 * @returns {Boolean} true if the sound is playable, false otherwise 454 */ 455 tQuery.WebAudio.Sound.prototype.isPlayable = function(){ 456 return this._source.buffer ? true : false; 457 }; 458 459 /** 460 * play the sound 461 * 462 * @param {Number} [time] time when to play the sound 463 */ 464 tQuery.WebAudio.Sound.prototype.play = function(time){ 465 if( time === undefined ) time = 0; 466 // clone the bufferSource 467 var clonedNode = this._chain.cloneBufferSource(); 468 // set the noteOn 469 clonedNode.noteOn(time); 470 // create the source object 471 var source = { 472 node : clonedNode, 473 stop : function(time){ 474 if( time === undefined ) time = 0; 475 this.node.noteOff(time); 476 return source; // for chained API 477 } 478 } 479 // return it 480 return source; 481 }; 482 483 /** 484 * getter/setter on the volume 485 * 486 * @param {Number} [value] the value to set, if not provided, get current value 487 */ 488 tQuery.WebAudio.Sound.prototype.volume = function(value){ 489 if( value === undefined ) return this._gainNode.gain.value; 490 this._gainNode.gain.value = value; 491 return this; // for chained API 492 }; 493 494 495 /** 496 * getter/setter on the loop 497 * 498 * @param {Number} [value] the value to set, if not provided, get current value 499 */ 500 tQuery.WebAudio.Sound.prototype.loop = function(value){ 501 if( value === undefined ) return this._source.loop; 502 this._source.loop = value; 503 return this; // for chained API 504 }; 505 506 /** 507 * Set parameter for the pannerCone 508 * 509 * @param {Number} innerAngle the inner cone hangle in radian 510 * @param {Number} outerAngle the outer cone hangle in radian 511 * @param {Number} outerGain the gain to apply when in the outerCone 512 */ 513 tQuery.WebAudio.Sound.prototype.pannerCone = function(innerAngle, outerAngle, outerGain) 514 { 515 this._panner.coneInnerAngle = innerAngle * 180 / Math.PI; 516 this._panner.coneOuterAngle = outerAngle * 180 / Math.PI; 517 this._panner.coneOuterGain = outerGain; 518 return this; // for chained API 519 }; 520 521 /** 522 * getter/setter on the pannerConeInnerAngle 523 * 524 * @param {Number} value the angle in radian 525 */ 526 tQuery.WebAudio.Sound.prototype.pannerConeInnerAngle = function(value){ 527 if( value === undefined ) return this._panner.coneInnerAngle / 180 * Math.PI; 528 this._panner.coneInnerAngle = value * 180 / Math.PI; 529 return this; // for chained API 530 }; 531 532 /** 533 * getter/setter on the pannerConeOuterAngle 534 * 535 * @param {Number} value the angle in radian 536 */ 537 tQuery.WebAudio.Sound.prototype.pannerConeOuterAngle = function(value){ 538 if( value === undefined ) return this._panner.coneOuterAngle / 180 * Math.PI; 539 this._panner.coneOuterAngle = value * 180 / Math.PI; 540 return this; // for chained API 541 }; 542 543 /** 544 * getter/setter on the pannerConeOuterGain 545 * 546 * @param {Number} value the value 547 */ 548 tQuery.WebAudio.Sound.prototype.pannerConeOuterGain = function(value){ 549 if( value === undefined ) return this._panner.coneOuterGain; 550 this._panner.coneOuterGain = value; 551 return this; // for chained API 552 }; 553 554 /** 555 * compute the amplitude of the sound (not sure at all it is the proper term) 556 * 557 * @param {Number} width the number of frequencyBin to take into account 558 * @returns {Number} return the amplitude of the sound 559 */ 560 tQuery.WebAudio.Sound.prototype.amplitude = function(width) 561 { 562 // handle paramerter 563 width = width !== undefined ? width : 2; 564 // inint variable 565 var analyser = this._analyser; 566 var freqByte = new Uint8Array(analyser.frequencyBinCount); 567 // get the frequency data 568 analyser.getByteFrequencyData(freqByte); 569 // compute the sum 570 var sum = 0; 571 for(var i = 0; i < width; i++){ 572 sum += freqByte[i]; 573 } 574 // complute the amplitude 575 var amplitude = sum / (width*256-1); 576 // return ampliture 577 return amplitude; 578 } 579 580 ////////////////////////////////////////////////////////////////////////////////// 581 // TODO put that in a plugin // 582 ////////////////////////////////////////////////////////////////////////////////// 583 584 /** 585 * Update the source with object3d. usefull for positional sounds 586 * 587 * @param {THREE.Object3D} object3d the object which originate the source 588 * @param {Number} deltaTime the number of seconds since last update 589 */ 590 tQuery.WebAudio.Sound.prototype.updateWithObject3d = function(object3d, deltaTime){ 591 // sanity check on parameters 592 console.assert( object3d instanceof THREE.Object3D ); 593 console.assert( typeof(deltaTime) === 'number' ); 594 595 // ensure object3d.matrixWorld is up to date 596 object3d.updateMatrixWorld(); 597 598 this.updateWithMatrix4(object3d.matrixWorld, deltaTime); 599 600 return this; // for chained API 601 } 602 603 /** 604 * Update the source with a matrixWorld. usefull for positional sounds 605 * 606 * @param {THREE.Matrix4} matrixWorld the matrixWorld describing the position of the sound 607 * @param {Number} deltaTime the number of seconds since last update 608 */ 609 tQuery.WebAudio.Sound.prototype.updateWithMatrix4 = function(matrixWorld, deltaTime){ 610 // sanity check on parameters 611 console.assert( matrixWorld instanceof THREE.Matrix4 ); 612 console.assert( typeof(deltaTime) === 'number' ); 613 614 //////////////////////////////////////////////////////////////////////// 615 // set position 616 var position = matrixWorld.getPosition(); 617 this._panner.setPosition(position.x, position.y, position.z); 618 619 //////////////////////////////////////////////////////////////////////// 620 // set orientation 621 var vOrientation= new THREE.Vector3(0,0,1); 622 var mOrientation= matrixWorld.clone(); 623 // zero the translation 624 mOrientation.setPosition({x : 0, y: 0, z: 0}); 625 // Multiply the 0,0,1 vector by the world matrix and normalize the result. 626 mOrientation.multiplyVector3(vOrientation); 627 vOrientation.normalize(); 628 // Set panner orientation 629 this._panner.setOrientation(vOrientation.x, vOrientation.y, vOrientation.z); 630 631 //////////////////////////////////////////////////////////////////////// 632 // set velocity 633 if( this._prevPos === undefined ){ 634 this._prevPos = matrixWorld.getPosition().clone(); 635 }else{ 636 var position = matrixWorld.getPosition(); 637 var velocity = position.clone().subSelf(this._prevPos).divideScalar(deltaTime); 638 this._prevPos = matrixWorld.getPosition().clone(); 639 this._panner.setVelocity(velocity.x, velocity.y, velocity.z); 640 } 641 } 642 643 ////////////////////////////////////////////////////////////////////////////////// 644 // // 645 ////////////////////////////////////////////////////////////////////////////////// 646 647 /** 648 * Load a sound 649 * 650 * @param {String} url the url of the sound to load 651 * @param {Function} callback function to notify once the url is loaded (optional) 652 */ 653 tQuery.WebAudio.Sound.prototype.load = function(url, callback){ 654 this._loadAndDecodeSound(url, function(buffer){ 655 this._source.buffer = buffer; 656 callback && callback(this); 657 }.bind(this), function(){ 658 console.warn("unable to load sound "+url); 659 }); 660 return this; // for chained API 661 }; 662 663 /** 664 * Load and decode a sound 665 * 666 * @param {String} url the url where to get the sound 667 * @param {Function} onLoad the function called when the sound is loaded and decoded (optional) 668 * @param {Function} onError the function called when an error occured (optional) 669 */ 670 tQuery.WebAudio.Sound.prototype._loadAndDecodeSound = function(url, onLoad, onError){ 671 var context = this._context; 672 var request = new XMLHttpRequest(); 673 request.open('GET', url, true); 674 request.responseType = 'arraybuffer'; 675 // Decode asynchronously 676 request.onload = function() { 677 context.decodeAudioData(request.response, function(buffer) { 678 onLoad && onLoad(buffer); 679 }, function(){ 680 onError && onError(); 681 }); 682 }; 683 // actually start the request 684 request.send(); 685 } 686