1 /**************************************************************************** 2 Copyright (c) 2010-2012 cocos2d-x.org 3 Copyright (c) 2008-2010 Ricardo Quesada 4 Copyright (c) 2011 Zynga Inc. 5 6 http://www.cocos2d-x.org 7 8 Permission is hereby granted, free of charge, to any person obtaining a copy 9 of this software and associated documentation files (the "Software"), to deal 10 in the Software without restriction, including without limitation the rights 11 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 copies of the Software, and to permit persons to whom the Software is 13 furnished to do so, subject to the following conditions: 14 15 The above copyright notice and this permission notice shall be included in 16 all copies or substantial portions of the Software. 17 18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 THE SOFTWARE. 25 ****************************************************************************/ 26 27 var cc = cc || {}; 28 29 30 /** 31 * A simple Audio Engine engine API. 32 * @class 33 * @extends cc.Class 34 */ 35 cc.AudioEngine = cc.Class.extend(/** @lends cc.AudioEngine# */{ 36 _audioID:0, 37 _audioIDList:null, 38 ctor:function(){ 39 this._audioIDList = {}; 40 }, 41 /** 42 * Check each type to see if it can be played by current browser 43 * @param {Object} capabilities The results are filled into this dict 44 * @protected 45 */ 46 _checkCanPlay: function(capabilities) { 47 var au = document.createElement('audio'); 48 if (au.canPlayType) { 49 // <audio> tag is supported, go on 50 var _check = function(typeStr) { 51 var result = au.canPlayType(typeStr); 52 return result != "no" && result != ""; 53 }; 54 55 capabilities.mp3 = _check("audio/mpeg"); 56 capabilities.mp4 = _check("audio/mp4"); 57 capabilities.m4a = _check("audio/x-m4a") || _check("audio/aac"); 58 capabilities.ogg = _check('audio/ogg; codecs="vorbis"'); 59 capabilities.wav = _check('audio/wav; codecs="1"'); 60 } else { 61 // <audio> tag is not supported, nothing is supported 62 var formats = ['mp3', 'mp4', 'm4a', 'ogg', 'wav']; 63 for (var idx in formats) { 64 capabilities[formats[idx]] = false; 65 } 66 } 67 }, 68 69 /** 70 * Helper function for cutting out the extension from the path 71 * @param {String} fullpath 72 * @return {String|null} path without ext name 73 * @protected 74 */ 75 _getPathWithoutExt: function (fullpath) { 76 if (typeof(fullpath) != "string") { 77 return null; 78 } 79 var endPos = fullpath.lastIndexOf("."); 80 if (endPos !== -1) 81 return fullpath.substring(0, endPos); 82 return fullpath; 83 }, 84 85 /** 86 * Helper function for extracting the extension from the path 87 * @param {String} fullpath 88 * @protected 89 */ 90 _getExtFromFullPath: function (fullpath) { 91 var startPos = fullpath.lastIndexOf("."); 92 if (startPos !== -1) { 93 return fullpath.substring(startPos + 1, fullpath.length); 94 } 95 return -1; 96 } 97 }); 98 99 100 101 /** 102 * the entity stored in soundList and effectList, containing the audio element and the extension name. 103 * used in cc.SimpleAudioEngine 104 */ 105 cc.SimpleSFX = function (audio, ext) { 106 this.audio = audio; 107 this.ext = ext || ".ogg"; 108 }; 109 110 /** 111 * The Audio Engine implementation via <audio> tag in HTML5. 112 * @class 113 * @extends cc.AudioEngine 114 */ 115 cc.SimpleAudioEngine = cc.AudioEngine.extend(/** @lends cc.SimpleAudioEngine# */{ 116 _supportedFormat:null, 117 _effectList:{}, 118 _soundList:{}, 119 _playingMusic:null, 120 _effectsVolume:1, 121 _maxAudioInstance:10, 122 _capabilities:null, 123 _soundSupported:false, 124 _canPlay:true, 125 126 /** 127 * Constructor 128 */ 129 ctor:function () { 130 cc.AudioEngine.prototype.ctor.call(this); 131 132 this._supportedFormat = []; 133 this._effectList = {}; 134 this._soundList = {}; 135 136 this._capabilities = { mp3: false, ogg: false, wav: false, mp4: false, m4a: false}; 137 var locCapabilities = this._capabilities; 138 this._checkCanPlay(locCapabilities); 139 140 // enable sound if any of the audio format is supported 141 this._soundSupported = locCapabilities.mp3 || locCapabilities.mp4 142 || locCapabilities.m4a || locCapabilities.ogg 143 || locCapabilities.wav; 144 145 var ua = navigator.userAgent; 146 if(/Mobile/.test(ua) && (/iPhone OS/.test(ua)||/iPad/.test(ua)||/Firefox/.test(ua)) || /MSIE/.test(ua)){ 147 this._canPlay = false; 148 } 149 }, 150 151 /** 152 * Initialize sound type 153 * @return {Boolean} 154 */ 155 init:function () { 156 // detect the prefered audio format 157 this._getSupportedAudioFormat(); 158 return this._soundSupported; 159 }, 160 161 /** 162 * Preload music resource. 163 * @param {String} path 164 */ 165 preloadMusic:function(path){ 166 this.preloadSound(path); 167 }, 168 169 /** 170 * Preload effect resource. 171 * @param {String} path 172 */ 173 preloadEffect:function(path){ 174 this.preloadSound(path); 175 }, 176 177 /** 178 * Preload music resource.<br /> 179 * This method is called when cc.Loader preload resources. 180 * @param {String} path The path of the music file with filename extension. 181 */ 182 preloadSound:function (path) { 183 if (this._soundSupported) { 184 var extName = this._getExtFromFullPath(path); 185 var keyname = this._getPathWithoutExt(path); 186 if (this.isFormatSupported(extName) && !this._soundList.hasOwnProperty(keyname)) { 187 if(this._canPlay){ 188 var sfxCache = new cc.SimpleSFX(); 189 sfxCache.ext = extName; 190 sfxCache.audio = new Audio(path); 191 sfxCache.audio.preload = 'auto'; 192 var soundPreloadCanplayHandler = function (e) { 193 cc.Loader.getInstance().onResLoaded(); 194 this.removeEventListener('canplaythrough', soundPreloadCanplayHandler, false); 195 this.removeEventListener('error', soundPreloadErrorHandler, false); 196 }; 197 var soundPreloadErrorHandler = function (e) { 198 cc.Loader.getInstance().onResLoadingErr(e.srcElement.src); 199 this.removeEventListener('canplaythrough', soundPreloadCanplayHandler, false); 200 this.removeEventListener('error', soundPreloadErrorHandler, false); 201 }; 202 sfxCache.audio.addEventListener('canplaythrough', soundPreloadCanplayHandler, false); 203 sfxCache.audio.addEventListener("error", soundPreloadErrorHandler, false); 204 205 this._soundList[keyname] = sfxCache; 206 sfxCache.audio.load(); 207 return; 208 } 209 } 210 } 211 cc.Loader.getInstance().onResLoaded(); 212 }, 213 214 /** 215 * Play music. 216 * @param {String} path The path of the music file without filename extension. 217 * @param {Boolean} loop Whether the music loop or not. 218 * @example 219 * //example 220 * cc.AudioEngine.getInstance().playMusic(path, false); 221 */ 222 playMusic:function (path, loop) { 223 if (!this._soundSupported) 224 return; 225 226 var keyname = this._getPathWithoutExt(path); 227 var extName = this._getExtFromFullPath(path); 228 var au; 229 230 var locSoundList = this._soundList; 231 if (locSoundList.hasOwnProperty(this._playingMusic)) { 232 locSoundList[this._playingMusic].audio.pause(); 233 } 234 235 this._playingMusic = keyname; 236 if (locSoundList.hasOwnProperty(this._playingMusic)) { 237 au = locSoundList[this._playingMusic].audio; 238 } else { 239 var sfxCache = new cc.SimpleSFX(); 240 sfxCache.ext = extName; 241 au = sfxCache.audio = new Audio(path); 242 sfxCache.audio.preload = 'auto'; 243 locSoundList[keyname] = sfxCache; 244 sfxCache.audio.load(); 245 } 246 247 au.addEventListener("pause", this._musicListener , false); 248 249 au.loop = loop || false; 250 au.play(); 251 cc.AudioEngine.isMusicPlaying = true; 252 }, 253 254 _musicListener:function(e){ 255 cc.AudioEngine.isMusicPlaying = false; 256 this.removeEventListener('pause', arguments.callee, false); 257 }, 258 259 /** 260 * Stop playing music. 261 * @param {Boolean} releaseData If release the music data or not.As default value is false. 262 * @example 263 * //example 264 * cc.AudioEngine.getInstance().stopMusic(); 265 */ 266 stopMusic:function (releaseData) { 267 var locSoundList = this._soundList, locPlayingMusic = this._playingMusic; 268 if (locSoundList.hasOwnProperty(locPlayingMusic)) { 269 var au = locSoundList[locPlayingMusic].audio; 270 au.pause(); 271 au.currentTime = au.duration; 272 if (releaseData) { 273 delete locSoundList[locPlayingMusic]; 274 } 275 cc.AudioEngine.isMusicPlaying = false; 276 } 277 }, 278 279 /** 280 * Pause playing music. 281 * @example 282 * //example 283 * cc.AudioEngine.getInstance().pauseMusic(); 284 */ 285 pauseMusic:function () { 286 if (this._soundList.hasOwnProperty(this._playingMusic)) { 287 var au = this._soundList[this._playingMusic].audio; 288 au.pause(); 289 cc.AudioEngine.isMusicPlaying = false; 290 } 291 }, 292 293 /** 294 * Resume playing music. 295 * @example 296 * //example 297 * cc.AudioEngine.getInstance().resumeMusic(); 298 */ 299 resumeMusic:function () { 300 if (this._soundList.hasOwnProperty(this._playingMusic)) { 301 var au = this._soundList[this._playingMusic].audio; 302 au.play(); 303 au.addEventListener("pause", this._musicListener , false); 304 cc.AudioEngine.isMusicPlaying = true; 305 } 306 }, 307 308 /** 309 * Rewind playing music. 310 * @example 311 * //example 312 * cc.AudioEngine.getInstance().rewindMusic(); 313 */ 314 rewindMusic:function () { 315 if (this._soundList.hasOwnProperty(this._playingMusic)) { 316 var au = this._soundList[this._playingMusic].audio; 317 au.currentTime = 0; 318 au.play(); 319 au.addEventListener("pause", this._musicListener , false); 320 cc.AudioEngine.isMusicPlaying = true; 321 } 322 }, 323 324 willPlayMusic:function () { 325 return false; 326 }, 327 328 /** 329 * The volume of the music max value is 1.0,the min value is 0.0 . 330 * @return {Number} 331 * @example 332 * //example 333 * var volume = cc.AudioEngine.getInstance().getMusicVolume(); 334 */ 335 getMusicVolume:function () { 336 if (this._soundList.hasOwnProperty(this._playingMusic)) { 337 return this._soundList[this._playingMusic].audio.volume; 338 } 339 return 0; 340 }, 341 342 /** 343 * Set the volume of music. 344 * @param {Number} volume Volume must be in 0.0~1.0 . 345 * @example 346 * //example 347 * cc.AudioEngine.getInstance().setMusicVolume(0.5); 348 */ 349 setMusicVolume:function (volume) { 350 if (this._soundList.hasOwnProperty(this._playingMusic)) { 351 var music = this._soundList[this._playingMusic].audio; 352 if (volume > 1) { 353 music.volume = 1; 354 } else if (volume < 0) { 355 music.volume = 0; 356 } else { 357 music.volume = volume; 358 } 359 } 360 }, 361 362 /** 363 * Whether the music is playing. 364 * @return {Boolean} If is playing return true,or return false. 365 * @example 366 * //example 367 * if (cc.AudioEngine.getInstance().isMusicPlaying()) { 368 * cc.log("music is playing"); 369 * } 370 * else { 371 * cc.log("music is not playing"); 372 * } 373 */ 374 isMusicPlaying: function () { 375 return cc.AudioEngine.isMusicPlaying; 376 }, 377 378 /** 379 * Play sound effect. 380 * @param {String} path The path of the sound effect with filename extension. 381 * @param {Boolean} loop Whether to loop the effect playing, default value is false 382 * @return {Number|null} the audio id 383 * @example 384 * //example 385 * var soundId = cc.AudioEngine.getInstance().playEffect(path); 386 */ 387 playEffect: function (path, loop) { 388 if (!this._soundSupported) 389 return null; 390 391 var keyname = this._getPathWithoutExt(path), actExt; 392 if (this._soundList.hasOwnProperty(keyname)) { 393 actExt = this._soundList[keyname].ext; 394 } else { 395 actExt = this._getExtFromFullPath(path); 396 } 397 398 var reclaim = this._getEffectList(keyname), au; 399 if (reclaim.length > 0) { 400 for (var i = 0; i < reclaim.length; i++) { 401 //if one of the effect ended, play it 402 if (reclaim[i].ended) { 403 au = reclaim[i]; 404 au.currentTime = 0; 405 if (window.chrome) 406 au.load(); 407 break; 408 } 409 } 410 } 411 412 if (!au) { 413 if (reclaim.length >= this._maxAudioInstance) { 414 cc.log("Error: " + path + " greater than " + this._maxAudioInstance); 415 return path; 416 } 417 au = new Audio(keyname + "." + actExt); 418 au.volume = this._effectsVolume; 419 reclaim.push(au); 420 } 421 422 if (loop) 423 au.loop = loop; 424 au.play(); 425 var audioID = this._audioID++; 426 this._audioIDList[audioID] = au; 427 return audioID; 428 }, 429 430 /** 431 *The volume of the effects max value is 1.0,the min value is 0.0 . 432 * @return {Number} 433 * @example 434 * //example 435 * var effectVolume = cc.AudioEngine.getInstance().getEffectsVolume(); 436 */ 437 getEffectsVolume:function () { 438 return this._effectsVolume; 439 }, 440 441 /** 442 * Set the volume of sound effecs. 443 * @param {Number} volume Volume must be in 0.0~1.0 . 444 * @example 445 * //example 446 * cc.AudioEngine.getInstance().setEffectsVolume(0.5); 447 */ 448 setEffectsVolume:function (volume) { 449 if (volume > 1) 450 this._effectsVolume = 1; 451 else if (volume < 0) 452 this._effectsVolume = 0; 453 else 454 this._effectsVolume = volume; 455 456 var tmpArr, au, locEffectList = this._effectList; 457 for (var i in locEffectList) { 458 tmpArr = locEffectList[i]; 459 if (tmpArr.length > 0) { 460 for (var j = 0; j < tmpArr.length; j++) { 461 au = tmpArr[j]; 462 au.volume = this._effectsVolume; 463 } 464 } 465 } 466 }, 467 468 /** 469 * Pause playing sound effect. 470 * @param {Number} audioID The return value of function playEffect. 471 * @example 472 * //example 473 * cc.AudioEngine.getInstance().pauseEffect(audioID); 474 */ 475 pauseEffect:function (audioID) { 476 if (audioID == null) return; 477 478 if (this._audioIDList.hasOwnProperty(audioID)) { 479 var au = this._audioIDList[audioID]; 480 if (!au.ended) { 481 au.pause(); 482 } 483 } 484 }, 485 486 /** 487 * Pause all playing sound effect. 488 * @example 489 * //example 490 * cc.AudioEngine.getInstance().pauseAllEffects(); 491 */ 492 pauseAllEffects:function () { 493 var tmpArr, au; 494 var locEffectList = this._effectList; 495 for (var i in locEffectList) { 496 tmpArr = locEffectList[i]; 497 for (var j = 0; j < tmpArr.length; j++) { 498 au = tmpArr[j]; 499 if (!au.ended) 500 au.pause(); 501 } 502 } 503 }, 504 505 /** 506 * Resume playing sound effect. 507 * @param {Number} audioID The return value of function playEffect. 508 * @audioID 509 * //example 510 * cc.AudioEngine.getInstance().resumeEffect(audioID); 511 */ 512 resumeEffect:function (audioID) { 513 if (audioID == null) return; 514 515 if (this._audioIDList.hasOwnProperty(audioID)) { 516 var au = this._audioIDList[audioID]; 517 if (!au.ended) 518 au.play(); 519 } 520 }, 521 522 /** 523 * Resume all playing sound effect 524 * @example 525 * //example 526 * cc.AudioEngine.getInstance().resumeAllEffects(); 527 */ 528 resumeAllEffects:function () { 529 var tmpArr, au; 530 var locEffectList = this._effectList; 531 for (var i in locEffectList) { 532 tmpArr = locEffectList[i]; 533 if (tmpArr.length > 0) { 534 for (var j = 0; j < tmpArr.length; j++) { 535 au = tmpArr[j]; 536 if (!au.ended) 537 au.play(); 538 } 539 } 540 } 541 }, 542 543 /** 544 * Stop playing sound effect. 545 * @param {Number} audioID The return value of function playEffect. 546 * @example 547 * //example 548 * cc.AudioEngine.getInstance().stopEffect(audioID); 549 */ 550 stopEffect:function (audioID) { 551 if (audioID == null) return; 552 553 if (this._audioIDList.hasOwnProperty(audioID)) { 554 var au = this._audioIDList[audioID]; 555 if (!au.ended) { 556 au.loop = false; 557 au.currentTime = au.duration; 558 } 559 } 560 }, 561 562 /** 563 * Stop all playing sound effects. 564 * @example 565 * //example 566 * cc.AudioEngine.getInstance().stopAllEffects(); 567 */ 568 stopAllEffects:function () { 569 var tmpArr, au, locEffectList = this._effectList; 570 for (var i in locEffectList) { 571 tmpArr = locEffectList[i]; 572 for (var j = 0; j < tmpArr.length; j++) { 573 au = tmpArr[j]; 574 if (!au.ended) { 575 au.loop = false; 576 au.currentTime = au.duration; 577 } 578 } 579 } 580 }, 581 582 /** 583 * Unload the preloaded effect from internal buffer 584 * @param {String} path 585 * @example 586 * //example 587 * cc.AudioEngine.getInstance().unloadEffect(EFFECT_FILE); 588 */ 589 unloadEffect:function (path) { 590 if (!path) return; 591 var keyname = this._getPathWithoutExt(path); 592 if (this._effectList.hasOwnProperty(keyname)) { 593 delete this._effectList[keyname]; 594 } 595 596 var au, pathName, locAudioIDList = this._audioIDList; 597 for (var k in locAudioIDList) { 598 au = locAudioIDList[k]; 599 pathName = this._getPathWithoutExt(au.src); 600 if(pathName.indexOf(keyname) > -1){ 601 this.stopEffect(k); 602 delete locAudioIDList[k]; 603 } 604 } 605 }, 606 607 _getEffectList:function (elt) { 608 var locEffectList = this._effectList; 609 if (locEffectList.hasOwnProperty(elt)) { 610 return locEffectList[elt]; 611 } else { 612 locEffectList[elt] = []; 613 return locEffectList[elt]; 614 } 615 }, 616 617 /** 618 * search in this._supportedFormat if ext is there 619 * @param {String} ext 620 * @returns {Boolean} 621 */ 622 isFormatSupported: function (ext) { 623 var tmpExt, locSupportedFormat = this._supportedFormat; 624 for (var i = 0; i < locSupportedFormat.length; i++) { 625 tmpExt = locSupportedFormat[i]; 626 if (tmpExt == ext) 627 return true; 628 } 629 return false; 630 }, 631 632 _getSupportedAudioFormat:function () { 633 // check for sound support by the browser 634 if (!this._soundSupported) { 635 return; 636 } 637 638 var formats = ['ogg', 'mp3', 'wav', 'mp4', 'm4a']; 639 for (var idx in formats) { 640 var name = formats[idx]; 641 if (this._capabilities[name]) { 642 this._supportedFormat.push(name); 643 } 644 } 645 } 646 }); 647 648 /** 649 * The entity stored in cc.WebAudioEngine, representing a sound object 650 */ 651 cc.WebAudioSFX = function(key, sourceNode, volumeNode, startTime, pauseTime) { 652 // the name of the relevant audio resource 653 this.key = key; 654 // the node used in Web Audio API in charge of the source data 655 this.sourceNode = sourceNode; 656 // the node used in Web Audio API in charge of volume 657 this.volumeNode = volumeNode; 658 /* 659 * when playing started from beginning, startTime is set to the current time of AudioContext.currentTime 660 * when paused, pauseTime is set to the current time of AudioContext.currentTime 661 * so how long the music has been played can be calculated 662 * these won't be used in other cases 663 */ 664 this.startTime = startTime || 0; 665 this.pauseTime = pauseTime || 0; 666 // by only sourceNode's playbackState, it cannot distinguish finished state from paused state 667 this.isPaused = false; 668 }; 669 670 /** 671 * The Audio Engine implementation via Web Audio API. 672 * @class 673 * @extends cc.AudioEngine 674 */ 675 cc.WebAudioEngine = cc.AudioEngine.extend(/** @lends cc.WebAudioEngine# */{ 676 // the Web Audio Context 677 _ctx: null, 678 // may be: mp3, ogg, wav, mp4, m4a 679 _supportedFormat: null, 680 // if sound is not enabled, this engine's init() will return false 681 _soundSupported: false, 682 // containing all binary buffers of loaded audio resources 683 _audioData: null, 684 /* 685 * Issue: When loading two resources with different suffixes asynchronously, the second one might start loading 686 * when the first one is already loading! 687 * To avoid this duplication, loading synchronously somehow doesn't work. _ctx.decodeAudioData() would throw an 688 * exception "DOM exception 12", it should be a bug of the browser. 689 * So just add something to mark some audios as LOADING so as to avoid duplication. 690 */ 691 _audiosLoading: null, 692 // the music being played, cc.WebAudioSFX, when null, no music is being played; when not null, it may be playing or paused 693 _music: null, 694 // the volume applied to the music 695 _musicVolume: 1, 696 // the effects being played: { key => [cc.WebAudioSFX] }, many effects of the same resource may be played simultaneously 697 _effects: null, 698 // the volume applied to all effects 699 _effectsVolume: 1, 700 701 /* 702 * _canPlay is a property in cc.SimpleAudioEngine, but not used in cc.WebAudioEngine. 703 * Only those which support Web Audio API will be using this cc.WebAudioEngine, so no need to add an extra check. 704 */ 705 // _canPlay: true, 706 /* 707 * _maxAudioInstance is also a property in cc.SimpleAudioEngine, but not used here 708 */ 709 // _maxAudioInstance: 10, 710 711 712 /** 713 * Constructor 714 */ 715 ctor: function() { 716 cc.AudioEngine.prototype.ctor.call(this); 717 this._supportedFormat = []; 718 this._audioData = {}; 719 this._audiosLoading = {}; 720 this._effects = {}; 721 }, 722 723 /** 724 * Initialization 725 * @return {Boolean} 726 */ 727 init: function() { 728 /* 729 * browser has proved to support Web Audio API in miniFramework.js 730 * only in that case will cc.WebAudioEngine be chosen to run, thus the following is guaranteed to work 731 */ 732 this._ctx = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext)(); 733 734 // gather capabilities information, enable sound if any of the audio format is supported 735 var capabilities = {}; 736 this._checkCanPlay(capabilities); 737 738 var formats = ['ogg', 'mp3', 'wav', 'mp4', 'm4a'], locSupportedFormat = this._supportedFormat; 739 for (var idx in formats) { 740 var name = formats[idx]; 741 if (capabilities[name]) 742 locSupportedFormat.push(name); 743 } 744 this._soundSupported = locSupportedFormat.length > 0; 745 return this._soundSupported; 746 }, 747 748 /** 749 * search in this._supportedFormat if ext is there 750 * @param {String} ext 751 * @returns {Boolean} 752 */ 753 isFormatSupported: function(ext) { 754 var locSupportedFormat = this._supportedFormat; 755 for (var idx in locSupportedFormat) { 756 if (ext === locSupportedFormat[idx]) 757 return true; 758 } 759 return false; 760 }, 761 762 /** 763 * Using XMLHttpRequest to retrieve the resource data from server. 764 * Not using cc.FileUtils.getByteArrayFromFile() because it is synchronous, 765 * so doing the retrieving here is more handful. 766 * @param {String} url The url to retrieve data 767 * @param {Object} onSuccess The callback to run when retrieving succeeds, the binary data array is passed into it 768 * @param {Object} onError The callback to run when retrieving fails 769 * @private 770 */ 771 _fetchData: function(url, onSuccess, onError) { 772 // currently, only the webkit browsers support Web Audio API, so it should be fine just writing like this. 773 var req = new window.XMLHttpRequest(); 774 req.open('GET', url, true); 775 req.responseType = 'arraybuffer'; 776 var engine = this; 777 req.onload = function() { 778 // when context decodes the array buffer successfully, call onSuccess 779 engine._ctx.decodeAudioData(req.response, onSuccess, onError); 780 }; 781 req.onerror = onError; 782 req.send(); 783 }, 784 785 /** 786 * Preload music resource. 787 * @param {String} path 788 */ 789 preloadMusic:function(path){ 790 this.preloadSound(path); 791 }, 792 793 /** 794 * Preload effect resource. 795 * @param {String} path 796 */ 797 preloadEffect:function(path){ 798 this.preloadSound(path); 799 }, 800 801 /** 802 * Preload music resource.<br /> 803 * This method is called when cc.Loader preload resources. 804 * @param {String} path The path of the music file with filename extension. 805 */ 806 preloadSound: function(path) { 807 if (!this._soundSupported) { 808 return; 809 } 810 811 var extName = this._getExtFromFullPath(path); 812 var keyName = this._getPathWithoutExt(path); 813 814 // not supported, already loaded, already loading 815 if (!this.isFormatSupported(extName) || keyName in this._audioData || keyName in this._audiosLoading) { 816 cc.Loader.getInstance().onResLoaded(); 817 return; 818 } 819 820 this._audiosLoading[keyName] = true; 821 var engine = this; 822 this._fetchData(path, function(buffer) { 823 // resource fetched, in @param buffer 824 engine._audioData[keyName] = buffer; 825 delete engine._audiosLoading[keyName]; 826 cc.Loader.getInstance().onResLoaded(); 827 }, function() { 828 // resource fetching failed 829 delete engine._audiosLoading[keyName]; 830 cc.Loader.getInstance().onResLoadingErr(path); 831 }); 832 }, 833 834 /** 835 * Init a new WebAudioSFX and play it, return this WebAudioSFX object 836 * assuming that key exists in this._audioData 837 * @param {String} key 838 * @param {Boolean} loop Default value is false 839 * @param {Number} volume 0.0 - 1.0, default value is 1.0 840 * @param {Number} offset Where to start playing (in seconds) 841 * @private 842 */ 843 _beginSound: function(key, loop, volume, offset) { 844 var sfxCache = new cc.WebAudioSFX(); 845 loop = loop || false; 846 volume = volume || 1; 847 offset = offset || 0; 848 849 sfxCache.key = key; 850 sfxCache.sourceNode = this._ctx.createBufferSource(); 851 sfxCache.sourceNode.buffer = this._audioData[key]; 852 sfxCache.sourceNode.loop = loop; 853 sfxCache.volumeNode = this._ctx.createGainNode(); 854 sfxCache.volumeNode.gain.value = volume; 855 856 sfxCache.sourceNode.connect(sfxCache.volumeNode); 857 sfxCache.volumeNode.connect(this._ctx.destination); 858 859 /* 860 * Safari on iOS 6 only supports noteOn(), noteGrainOn(), and noteOff() now.(iOS 6.1.3) 861 * The latest version of chrome has supported start() and stop() 862 * start() & stop() are specified in the latest specification (written on 04/26/2013) 863 * Reference: https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html 864 * noteOn(), noteGrainOn(), and noteOff() are specified in Draft 13 version (03/13/2012) 865 * Reference: http://www.w3.org/2011/audio/drafts/2WD/Overview.html 866 */ 867 if (sfxCache.sourceNode.start) { 868 // starting from offset means resuming from where it paused last time 869 sfxCache.sourceNode.start(0, offset); 870 } else if (sfxCache.sourceNode.noteGrainOn) { 871 var duration = sfxCache.sourceNode.buffer.duration; 872 if (loop) { 873 /* 874 * On Safari on iOS 6, if loop == true, the passed in @param duration will be the duration from now on. 875 * In other words, the sound will keep playing the rest of the music all the time. 876 * On latest chrome desktop version, the passed in duration will only be the duration in this cycle. 877 * Now that latest chrome would have start() method, it is prepared for iOS here. 878 */ 879 sfxCache.sourceNode.noteGrainOn(0, offset, duration); 880 } else { 881 sfxCache.sourceNode.noteGrainOn(0, offset, duration - offset); 882 } 883 } else { 884 // if only noteOn() is supported, resuming sound will NOT work 885 sfxCache.sourceNode.noteOn(0); 886 } 887 888 // currentTime - offset is necessary for pausing multiple times! 889 sfxCache.startTime = this._ctx.currentTime - offset; 890 sfxCache.pauseTime = sfxCache.startTime; 891 sfxCache.isPaused = false; 892 893 return sfxCache; 894 }, 895 896 /** 897 * <p> 898 * According to the spec: dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html <br/> 899 * const unsigned short UNSCHEDULED_STATE = 0; <br/> 900 * const unsigned short SCHEDULED_STATE = 1; <br/> 901 * const unsigned short PLAYING_STATE = 2; // this means it is playing <br/> 902 * const unsigned short FINISHED_STATE = 3; <br/> 903 * However, the older specification doesn't include this property, such as this one: http://www.w3.org/2011/audio/drafts/2WD/Overview.html 904 * </p> 905 * @param {Object} sfxCache Assuming not null 906 * @returns {Boolean} Whether sfxCache is playing or not 907 * @private 908 */ 909 _isSoundPlaying: function(sfxCache) { 910 return sfxCache.sourceNode.playbackState == 2; 911 }, 912 913 /** 914 * To distinguish 3 kinds of status for each sound (PLAYING, PAUSED, FINISHED), _isSoundPlaying() is not enough 915 * @param {Object} sfxCache Assuming not null 916 * @returns {Boolean} 917 * @private 918 */ 919 _isSoundPaused: function(sfxCache) { 920 // checking _isSoundPlaying() won't hurt 921 return this._isSoundPlaying(sfxCache) ? false : sfxCache.isPaused; 922 }, 923 924 /** 925 * Whether it is playing any music 926 * @return {Boolean} If is playing return true,or return false. 927 * @example 928 * //example 929 * if (cc.AudioEngine.getInstance().isMusicPlaying()) { 930 * cc.log("music is playing"); 931 * } 932 * else { 933 * cc.log("music is not playing"); 934 * } 935 */ 936 isMusicPlaying: function () { 937 /* 938 * cc.AudioEngine.isMusicPlaying property is not going to be used here in cc.WebAudioEngine 939 * that is only used in cc.SimpleAudioEngine 940 * WebAudioEngine uses Web Audio API which contains a playbackState property in AudioBufferSourceNode 941 * So there is also no need to be any method like setMusicPlaying(), it is done automatically 942 */ 943 return this._music ? this._isSoundPlaying(this._music) : false; 944 }, 945 946 /** 947 * Play music. 948 * @param {String} path The path of the music file without filename extension. 949 * @param {Boolean} loop Whether the music loop or not. 950 * @example 951 * //example 952 * cc.AudioEngine.getInstance().playMusic(path, false); 953 */ 954 playMusic: function (path, loop) { 955 var keyName = this._getPathWithoutExt(path); 956 var extName = this._getExtFromFullPath(path); 957 loop = loop || false; 958 959 if (this._music) { 960 // there is a music being played currently, stop it (may be paused) 961 this.stopMusic(); 962 } 963 964 if (keyName in this._audioData) { 965 // already loaded, just play it 966 this._music = this._beginSound(keyName, loop, this.getMusicVolume()); 967 } else if (this.isFormatSupported(extName) && !(keyName in this._audiosLoading)) { 968 // load now only if the type is supported and it is not being loaded currently 969 this._audiosLoading[keyName] = true; 970 var engine = this; 971 this._fetchData(path, function(buffer) { 972 // resource fetched, save it and call playMusic() again, this time it should be alright 973 engine._audioData[keyName] = buffer; 974 delete engine._audiosLoading[keyName]; 975 engine.playMusic(path, loop); 976 }, function() { 977 // resource fetching failed, doing nothing here 978 delete engine._audiosLoading[keyName]; 979 /* 980 * Potential Bug: if fetching data fails every time, loading will be tried again and again. 981 * Preloading would prevent this issue: if it fails to fetch, preloading procedure will not achieve 100%. 982 */ 983 }); 984 } 985 }, 986 987 /** 988 * Ends a sound, call stop() or noteOff() accordingly 989 * @param {Object} sfxCache Assuming not null 990 * @private 991 */ 992 _endSound: function(sfxCache) { 993 if (sfxCache.sourceNode.stop) { 994 sfxCache.sourceNode.stop(0); 995 } else { 996 sfxCache.sourceNode.noteOff(0); 997 } 998 // Do not call disconnect()! Otherwise the sourceNode's playbackState may not be updated correctly 999 // sfxCache.sourceNode.disconnect(); 1000 // sfxCache.volumeNode.disconnect(); 1001 }, 1002 1003 /** 1004 * Stop playing music. 1005 * @param {Boolean} releaseData If release the music data or not.As default value is false. 1006 * @example 1007 * //example 1008 * cc.AudioEngine.getInstance().stopMusic(); 1009 */ 1010 stopMusic: function(releaseData) { 1011 // can stop when it's playing/paused 1012 if (!this._music) { 1013 return; 1014 } 1015 1016 var key = this._music.key; 1017 this._endSound(this._music); 1018 this._music = null; 1019 1020 if (releaseData) { 1021 delete this._audioData[key]; 1022 } 1023 }, 1024 1025 /** 1026 * Used in pauseMusic() & pauseEffect() & pauseAllEffects() 1027 * @param {Object} sfxCache Assuming not null 1028 * @private 1029 */ 1030 _pauseSound: function(sfxCache) { 1031 sfxCache.pauseTime = this._ctx.currentTime; 1032 sfxCache.isPaused = true; 1033 this._endSound(sfxCache); 1034 }, 1035 1036 /** 1037 * Pause playing music. 1038 * @example 1039 * //example 1040 * cc.AudioEngine.getInstance().pauseMusic(); 1041 */ 1042 pauseMusic: function() { 1043 // can pause only when it's playing 1044 if (!this.isMusicPlaying()) { 1045 return; 1046 } 1047 1048 this._pauseSound(this._music); 1049 }, 1050 1051 /** 1052 * Used in resumeMusic() & resumeEffect() & resumeAllEffects() 1053 * @param {Object} paused The paused WebAudioSFX, assuming not null 1054 * @param {Number} volume Can be getMusicVolume() or getEffectsVolume() 1055 * @returns {Object} A new WebAudioSFX object representing the resumed sound 1056 * @private 1057 */ 1058 _resumeSound: function(paused, volume) { 1059 var key = paused.key; 1060 var loop = paused.sourceNode.loop; 1061 // the paused sound may have been playing several loops, (pauseTime - startTime) may be too large 1062 var offset = (paused.pauseTime - paused.startTime) % paused.sourceNode.buffer.duration; 1063 1064 return this._beginSound(key, loop, volume, offset); 1065 }, 1066 1067 /** 1068 * Resume playing music. 1069 * @example 1070 * //example 1071 * cc.AudioEngine.getInstance().resumeMusic(); 1072 */ 1073 resumeMusic: function() { 1074 // can resume only when it's paused 1075 if (!this._music || !this._isSoundPaused(this._music)) { 1076 return; 1077 } 1078 1079 this._music = this._resumeSound(this._music, this.getMusicVolume()); 1080 }, 1081 1082 /** 1083 * Rewind playing music. 1084 * @example 1085 * //example 1086 * cc.AudioEngine.getInstance().rewindMusic(); 1087 */ 1088 rewindMusic: function() { 1089 // can rewind when it's playing or paused 1090 if (!this._music) { 1091 return; 1092 } 1093 1094 var key = this._music.key; 1095 var loop = this._music.sourceNode.loop; 1096 var volume = this.getMusicVolume(); 1097 1098 this._endSound(this._music); 1099 this._music = this._beginSound(key, loop, volume); 1100 }, 1101 1102 willPlayMusic: function() { 1103 // TODO what is the purpose of this method? This is just a copy from cc.SimpleAudioEngine 1104 return false; 1105 }, 1106 1107 /** 1108 * The volume of the music max value is 1.0,the min value is 0.0 . 1109 * @return {Number} 1110 * @example 1111 * //example 1112 * var volume = cc.AudioEngine.getInstance().getMusicVolume(); 1113 */ 1114 getMusicVolume: function() { 1115 return this._musicVolume; 1116 }, 1117 1118 /** 1119 * update volume, used in setMusicVolume() or setEffectsVolume() 1120 * @param {Object} sfxCache Assuming not null 1121 * @param {Number} volume 1122 * @private 1123 */ 1124 _setSoundVolume: function(sfxCache, volume) { 1125 sfxCache.volumeNode.gain.value = volume; 1126 }, 1127 1128 /** 1129 * Set the volume of music. 1130 * @param {Number} volume Volume must be in 0.0~1.0 . 1131 * @example 1132 * //example 1133 * cc.AudioEngine.getInstance().setMusicVolume(0.5); 1134 */ 1135 setMusicVolume: function(volume) { 1136 if (volume > 1) { 1137 volume = 1; 1138 } else if (volume < 0) { 1139 volume = 0; 1140 } 1141 1142 if (this.getMusicVolume() == volume) { 1143 // it is the same, no need to update 1144 return; 1145 } 1146 1147 this._musicVolume = volume; 1148 if (this._music) { 1149 this._setSoundVolume(this._music, volume); 1150 } 1151 }, 1152 1153 /** 1154 * Play sound effect. 1155 * @param {String} path The path of the sound effect with filename extension. 1156 * @param {Boolean} loop Whether to loop the effect playing, default value is false 1157 * @example 1158 * //example 1159 * cc.AudioEngine.getInstance().playEffect(path); 1160 */ 1161 playEffect: function(path, loop) { 1162 var keyName = this._getPathWithoutExt(path); 1163 var extName = this._getExtFromFullPath(path); 1164 loop = loop || false; 1165 1166 if (keyName in this._audioData) { 1167 // the resource has been loaded, just play it 1168 var locEffects = this._effects; 1169 if (!(keyName in locEffects)) { 1170 locEffects[keyName] = []; 1171 } 1172 // a list of sound objects from the same resource 1173 var effectList = locEffects[keyName]; 1174 for (var idx in effectList) { 1175 var sfxCache = effectList[idx]; 1176 if (!this._isSoundPlaying(sfxCache) && !this._isSoundPaused(sfxCache)) { 1177 // not playing && not paused => it is finished, this position can be reused 1178 effectList[idx] = this._beginSound(keyName, loop, this.getEffectsVolume()); 1179 return path; 1180 } 1181 } 1182 // no new sound was created to replace an old one in the list, then just append one 1183 effectList.push(this._beginSound(keyName, loop, this.getEffectsVolume())); 1184 } else if (this.isFormatSupported(extName) && !(keyName in this._audiosLoading)) { 1185 // load now only if the type is supported and it is not being loaded currently 1186 this._audiosLoading[keyName] = true; 1187 var engine = this; 1188 this._fetchData(path, function(buffer) { 1189 // resource fetched, save it and call playEffect() again, this time it should be alright 1190 engine._audioData[keyName] = buffer; 1191 delete engine._audiosLoading[keyName]; 1192 engine.playEffect(path, loop); 1193 }, function() { 1194 // resource fetching failed, doing nothing here 1195 delete engine._audiosLoading[keyName]; 1196 /* 1197 * Potential Bug: if fetching data fails every time, loading will be tried again and again. 1198 * Preloading would prevent this issue: if it fails to fetch, preloading procedure will not achieve 100%. 1199 */ 1200 }); 1201 } 1202 1203 // cc.SimpleAudioEngine returns path, just do the same for backward compatibility. DO NOT rely on this, though! 1204 var audioID = this._audioID++; 1205 this._audioIDList[audioID] = this._effects[keyName]; 1206 1207 return audioID; 1208 }, 1209 1210 /** 1211 * The volume of the effects max value is 1.0,the min value is 0.0 . 1212 * @return {Number} 1213 * @example 1214 * //example 1215 * var effectVolume = cc.AudioEngine.getInstance().getEffectsVolume(); 1216 */ 1217 getEffectsVolume: function() { 1218 return this._effectsVolume; 1219 }, 1220 1221 /** 1222 * Set the volume of sound effects. 1223 * @param {Number} volume Volume must be in 0.0~1.0 . 1224 * @example 1225 * //example 1226 * cc.AudioEngine.getInstance().setEffectsVolume(0.5); 1227 */ 1228 setEffectsVolume: function(volume) { 1229 if (volume > 1) { 1230 volume = 1; 1231 } else if (volume < 0) { 1232 volume = 0; 1233 } 1234 if (this.getEffectsVolume() == volume) { 1235 // it is the same, no need to update 1236 return; 1237 } 1238 1239 this._effectsVolume = volume; 1240 var locEffects = this._effects; 1241 for (var key in locEffects) { 1242 var effectList = locEffects[key]; 1243 for (var idx in effectList) { 1244 this._setSoundVolume(effectList[idx], volume); 1245 } 1246 } 1247 }, 1248 1249 /** 1250 * Used in pauseEffect() and pauseAllEffects() 1251 * @param {Object} effectList A list of sounds, each sound may be playing/paused/finished 1252 * @private 1253 */ 1254 _pauseSoundList: function(effectList) { 1255 for (var idx in effectList) { 1256 var sfxCache = effectList[idx]; 1257 if (this._isSoundPlaying(sfxCache)) { 1258 this._pauseSound(sfxCache); 1259 } 1260 } 1261 }, 1262 1263 /** 1264 * Pause playing sound effect. 1265 * @param {Number} audioID The return value of function playEffect. 1266 * @example 1267 * //example 1268 * cc.AudioEngine.getInstance().pauseEffect(audioID); 1269 */ 1270 pauseEffect: function(audioID) { 1271 if (audioID == null) { 1272 return; 1273 } 1274 1275 if (this._audioIDList.hasOwnProperty(audioID)) { 1276 this._pauseSoundList(this._audioIDList[audioID]); 1277 } 1278 }, 1279 1280 /** 1281 * Pause all playing sound effect. 1282 * @example 1283 * //example 1284 * cc.AudioEngine.getInstance().pauseAllEffects(); 1285 */ 1286 pauseAllEffects: function() { 1287 for (var key in this._effects) { 1288 this._pauseSoundList(this._effects[key]); 1289 } 1290 }, 1291 1292 /** 1293 * Used in resumeEffect() and resumeAllEffects() 1294 * @param {Object} effectList A list of sounds, each sound may be playing/paused/finished 1295 * @param {Number} volume 1296 * @private 1297 */ 1298 _resumeSoundList: function(effectList, volume) { 1299 for (var idx in effectList) { 1300 var sfxCache = effectList[idx]; 1301 if (this._isSoundPaused(sfxCache)) { 1302 effectList[idx] = this._resumeSound(sfxCache, volume); 1303 } 1304 } 1305 }, 1306 1307 /** 1308 * Resume playing sound effect. 1309 * @param {Number} audioID The return value of function playEffect. 1310 * @example 1311 * //example 1312 * cc.AudioEngine.getInstance().resumeEffect(audioID); 1313 */ 1314 resumeEffect: function(audioID) { 1315 if (audioID == null) { 1316 return; 1317 } 1318 1319 if (this._audioIDList.hasOwnProperty(audioID)) { 1320 this._resumeSoundList(this._audioIDList[audioID], this.getEffectsVolume()); 1321 } 1322 }, 1323 1324 /** 1325 * Resume all playing sound effect 1326 * @example 1327 * //example 1328 * cc.AudioEngine.getInstance().resumeAllEffects(); 1329 */ 1330 resumeAllEffects: function() { 1331 for (var key in this._effects) { 1332 this._resumeSoundList(this._effects[key], this.getEffectsVolume()); 1333 } 1334 }, 1335 1336 /** 1337 * Stop playing sound effect. 1338 * @param {Number} audioID The return value of function playEffect. 1339 * @example 1340 * //example 1341 * cc.AudioEngine.getInstance().stopEffect(audioID); 1342 */ 1343 stopEffect: function(audioID) { 1344 if (audioID == null) { 1345 return; 1346 } 1347 1348 if (this._audioIDList.hasOwnProperty(audioID)) { 1349 this._endSound(this._audioIDList[audioID]); 1350 } 1351 }, 1352 1353 /** 1354 * Stop all playing sound effects. 1355 * @example 1356 * //example 1357 * cc.AudioEngine.getInstance().stopAllEffects(); 1358 */ 1359 stopAllEffects: function() { 1360 var locEffects = this._effects; 1361 for (var key in locEffects) { 1362 var effectList = locEffects[key]; 1363 for (var idx in effectList) { 1364 this._endSound(effectList[idx]); 1365 } 1366 /* 1367 * Another way is to set this._effects = {} outside this for loop. 1368 * However, the cc.Class.extend() put all properties in the prototype. 1369 * If I reassign a new {} to it, that will be appear in the instance. 1370 * In other words, the dict in prototype won't release its children. 1371 */ 1372 delete locEffects[key]; 1373 } 1374 }, 1375 1376 /** 1377 * Unload the preloaded effect from internal buffer 1378 * @param {String} path 1379 * @example 1380 * //example 1381 * cc.AudioEngine.getInstance().unloadEffect(EFFECT_FILE); 1382 */ 1383 unloadEffect: function(path) { 1384 if (!path) 1385 return; 1386 1387 var keyName = this._getPathWithoutExt(path); 1388 if (this._effects.hasOwnProperty(keyName)) { 1389 this.stopEffect(path); 1390 delete this._effects[keyname]; 1391 } 1392 1393 if (keyName in this._audioData) { 1394 delete this._audioData[keyName]; 1395 } 1396 } 1397 }); 1398 1399 cc.AudioEngine._instance = null; 1400 1401 cc.AudioEngine.isMusicPlaying = false; 1402 1403 /** 1404 * Get the shared Engine object, it will new one when first time be called. 1405 * @return {cc.AudioEngine} 1406 */ 1407 cc.AudioEngine.getInstance = function () { 1408 if (!this._instance) { 1409 var ua = navigator.userAgent; 1410 if (cc.Browser.supportWebAudio && (/iPhone OS/.test(ua)||/iPad/.test(ua))) { 1411 this._instance = new cc.WebAudioEngine(); 1412 } else { 1413 this._instance = new cc.SimpleAudioEngine(); 1414 } 1415 this._instance.init(); 1416 } 1417 return this._instance; 1418 }; 1419 1420 1421 /** 1422 * Stop all music and sound effects 1423 * @example 1424 * //example 1425 * cc.AudioEngine.end(); 1426 */ 1427 cc.AudioEngine.end = function () { 1428 if (this._instance) { 1429 this._instance.stopMusic(); 1430 this._instance.stopAllEffects(); 1431 } 1432 this._instance = null; 1433 }; 1434