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 * A simple Audio Engine engine API. 31 * @class 32 * @extends cc.Class 33 */ 34 cc.AudioEngine = cc.Class.extend(/** @lends cc.AudioEngine# */{ 35 _audioID:0, 36 _audioIDList:null, 37 _supportedFormat:null, 38 _soundSupported:false, // if sound is not enabled, this engine's init() will return false 39 _effectsVolume:1, // the volume applied to all effects 40 _playingMusic:null, // the music being played, when null, no music is being played; when not null, it may be playing or paused 41 _resPath : "", //root path for resources 42 _pausedPlayings: null, 43 44 ctor:function(){ 45 this._audioIDList = {}; 46 this._supportedFormat = []; 47 this._pausedPlayings = []; 48 }, 49 50 /** 51 * Set root path for music resources. 52 * @param resPath 53 */ 54 setResPath : function(resPath){ 55 if(!resPath || resPath.length == 0) return; 56 this._resPath = resPath.substring(resPath.length - 1) == "/" ? resPath : resPath + "/"; 57 }, 58 /** 59 * Check each type to see if it can be played by current browser 60 * @param {Object} capabilities The results are filled into this dict 61 * @protected 62 */ 63 _checkCanPlay: function(capabilities) { 64 var au = document.createElement('audio'); 65 if (au.canPlayType) { 66 // <audio> tag is supported, go on 67 var _check = function(typeStr) { 68 var result = au.canPlayType(typeStr); 69 return result != "no" && result != ""; 70 }; 71 72 capabilities["mp3"] = _check("audio/mpeg"); 73 capabilities["mp4"] = _check("audio/mp4"); 74 capabilities["m4a"] = _check("audio/x-m4a") || _check("audio/aac"); 75 capabilities["ogg"] = _check('audio/ogg; codecs="vorbis"'); 76 capabilities["wav"] = _check('audio/wav; codecs="1"'); 77 } else { 78 // <audio> tag is not supported, nothing is supported 79 var formats = ["mp3", "mp4", "m4a", "ogg", "wav"]; 80 for (var idx in formats) { 81 capabilities[formats[idx]] = false; 82 } 83 } 84 }, 85 86 /** 87 * Helper function for cutting out the extension from the path 88 * @param {String} fullpath 89 * @return {String|null} path without ext name 90 * @protected 91 */ 92 _getPathWithoutExt: function (fullpath) { 93 if (typeof(fullpath) != "string") { 94 return null; 95 } 96 var endPos = fullpath.lastIndexOf("."); 97 if (endPos !== -1) 98 return fullpath.substring(0, endPos); 99 return fullpath; 100 }, 101 102 /** 103 * Helper function for extracting the extension from the path 104 * @param {String} fullpath 105 * @protected 106 */ 107 _getExtFromFullPath: function (fullpath) { 108 var startPos = fullpath.lastIndexOf("."); 109 if (startPos !== -1) { 110 return fullpath.substring(startPos + 1, fullpath.length); 111 } 112 return -1; 113 }, 114 115 /** 116 * Indicates whether any background music can be played or not. 117 * @returns {boolean} <i>true</i> if the background music is playing, otherwise <i>false</i> 118 */ 119 willPlayMusic: function() { 120 return false; 121 }, 122 123 /** 124 * Preload music resource. 125 * @param {String} path 126 * @param {Function} selector 127 * @param {Object} target 128 */ 129 preloadMusic:function(path, selector, target){ 130 this.preloadSound(path, selector, target); 131 }, 132 133 /** 134 * Preload effect resource. 135 * @param {String} path 136 * @param {Function} selector 137 * @param {Object} target 138 */ 139 preloadEffect:function(path, selector, target){ 140 this.preloadSound(path, selector, target); 141 }, 142 143 /** 144 * search in this._supportedFormat if ext is there 145 * @param {String} ext 146 * @returns {Boolean} 147 */ 148 isFormatSupported: function (ext) { 149 var locSupportedFormat = this._supportedFormat; 150 for (var i = 0, len = locSupportedFormat.length; i < len; i++) { 151 if (locSupportedFormat[i] == ext) 152 return true; 153 } 154 return false; 155 }, 156 157 /** 158 * The volume of the effects max value is 1.0,the min value is 0.0 . 159 * @return {Number} 160 * @example 161 * //example 162 * var effectVolume = cc.AudioEngine.getInstance().getEffectsVolume(); 163 */ 164 getEffectsVolume: function() { 165 return this._effectsVolume; 166 } 167 }); 168 169 /** 170 * the entity stored in soundList and effectList, containing the audio element and the extension name. 171 * used in cc.SimpleAudioEngine 172 */ 173 cc.SimpleSFX = function (audio, ext) { 174 this.audio = audio; 175 this.ext = ext || ".ogg"; 176 }; 177 178 /** 179 * The Audio Engine implementation via <audio> tag in HTML5. 180 * @class 181 * @extends cc.AudioEngine 182 */ 183 cc.SimpleAudioEngine = cc.AudioEngine.extend(/** @lends cc.SimpleAudioEngine# */{ 184 _effectList:null, 185 _soundList:null, 186 _maxAudioInstance:5, 187 _canPlay:true, 188 _musicListenerBound:null, 189 _musicIsStopped: false, 190 191 /** 192 * Constructor 193 */ 194 ctor:function () { 195 cc.AudioEngine.prototype.ctor.call(this); 196 this._effectList = {}; 197 this._soundList = {}; 198 this._musicListenerBound = this._musicListener.bind(this); 199 var ua = navigator.userAgent; 200 if(/Mobile/.test(ua) && (/iPhone OS/.test(ua)||/iPad/.test(ua)||/Firefox/.test(ua)) || /MSIE/.test(ua)){ 201 this._canPlay = false; 202 } 203 }, 204 205 /** 206 * Initialize sound type 207 * @return {Boolean} 208 */ 209 init:function () { 210 // gather capabilities information, enable sound if any of the audio format is supported 211 var capabilities = {}; 212 this._checkCanPlay(capabilities); 213 214 var formats = ["ogg", "mp3", "wav", "mp4", "m4a"], locSupportedFormat = this._supportedFormat; 215 for (var idx in formats) { 216 var name = formats[idx]; 217 if (capabilities[name]) 218 locSupportedFormat.push(name); 219 } 220 this._soundSupported = locSupportedFormat.length > 0; 221 return this._soundSupported; 222 }, 223 224 /** 225 * Preload music resource.<br /> 226 * This method is called when cc.Loader preload resources. 227 * @param {String} path The path of the music file with filename extension. 228 * @param {Function} selector 229 * @param {Object} target 230 */ 231 preloadSound:function (path, selector, target) { 232 if (this._soundSupported) { 233 var realPath = this._resPath + path; 234 var extName = this._getExtFromFullPath(path); 235 var keyname = this._getPathWithoutExt(path); 236 if (!this._soundList[keyname] && this.isFormatSupported(extName)) { 237 if(this._canPlay){ 238 var sfxCache = new cc.SimpleSFX(); 239 sfxCache.ext = extName; 240 sfxCache.audio = new Audio(realPath); 241 sfxCache.audio.preload = 'auto'; 242 var soundPreloadCanplayHandler = function () { 243 cc.doCallback(selector, target); 244 this.removeEventListener('canplaythrough', soundPreloadCanplayHandler, false); 245 this.removeEventListener('error', soundPreloadErrorHandler, false); 246 }; 247 var soundPreloadErrorHandler = function (e) { 248 cc.doCallback(selector, target,e.srcElement.src); 249 this.removeEventListener('canplaythrough', soundPreloadCanplayHandler, false); 250 this.removeEventListener('error', soundPreloadErrorHandler, false); 251 }; 252 sfxCache.audio.addEventListener('canplaythrough', soundPreloadCanplayHandler, false); 253 sfxCache.audio.addEventListener("error", soundPreloadErrorHandler, false); 254 255 this._soundList[keyname] = sfxCache; 256 sfxCache.audio.load(); 257 return; 258 } 259 } 260 } 261 cc.doCallback(selector, target); 262 }, 263 264 /** 265 * Play music. 266 * @param {String} path The path of the music file without filename extension. 267 * @param {Boolean} loop Whether the music loop or not. 268 * @example 269 * //example 270 * cc.AudioEngine.getInstance().playMusic(path, false); 271 */ 272 playMusic:function (path, loop) { 273 if (!this._soundSupported) 274 return; 275 276 var keyname = this._getPathWithoutExt(path); 277 var extName = this._getExtFromFullPath(path); 278 var au; 279 280 var locSoundList = this._soundList; 281 if (locSoundList[this._playingMusic]) 282 locSoundList[this._playingMusic].audio.pause(); 283 284 this._playingMusic = keyname; 285 if (locSoundList[this._playingMusic]) 286 au = locSoundList[this._playingMusic].audio; 287 else { 288 var sfxCache = new cc.SimpleSFX(); 289 sfxCache.ext = extName; 290 au = sfxCache.audio = new Audio(path); 291 sfxCache.audio.preload = 'auto'; 292 locSoundList[keyname] = sfxCache; 293 sfxCache.audio.load(); 294 } 295 296 au.addEventListener("pause", this._musicListenerBound , false); 297 au.loop = loop || false; 298 au.play(); 299 cc.AudioEngine.isMusicPlaying = true; 300 this._musicIsStopped = false; 301 }, 302 303 _musicListener:function(e){ 304 cc.AudioEngine.isMusicPlaying = false; 305 if (this._soundList[this._playingMusic]) { 306 var au = this._soundList[this._playingMusic].audio; 307 au.removeEventListener('pause', this._musicListener, false); 308 } 309 }, 310 311 /** 312 * Stop playing music. 313 * @param {Boolean} releaseData If release the music data or not.As default value is false. 314 * @example 315 * //example 316 * cc.AudioEngine.getInstance().stopMusic(); 317 */ 318 stopMusic:function (releaseData) { 319 var locSoundList = this._soundList, locPlayingMusic = this._playingMusic; 320 if (locSoundList[locPlayingMusic]) { 321 var au = locSoundList[locPlayingMusic].audio; 322 au.pause(); 323 au.duration && (au.currentTime = au.duration); 324 if (releaseData) 325 delete locSoundList[locPlayingMusic]; 326 cc.AudioEngine.isMusicPlaying = false; 327 this._musicIsStopped = true; 328 } 329 }, 330 331 /** 332 * Pause playing music. 333 * @example 334 * //example 335 * cc.AudioEngine.getInstance().pauseMusic(); 336 */ 337 pauseMusic:function () { 338 if (!this._musicIsStopped && this._soundList[this._playingMusic]) { 339 var au = this._soundList[this._playingMusic].audio; 340 au.pause(); 341 cc.AudioEngine.isMusicPlaying = false; 342 } 343 }, 344 345 /** 346 * Resume playing music. 347 * @example 348 * //example 349 * cc.AudioEngine.getInstance().resumeMusic(); 350 */ 351 resumeMusic:function () { 352 if (!this._musicIsStopped && this._soundList[this._playingMusic]) { 353 var au = this._soundList[this._playingMusic].audio; 354 au.play(); 355 au.addEventListener("pause", this._musicListenerBound , false); 356 cc.AudioEngine.isMusicPlaying = true; 357 } 358 }, 359 360 /** 361 * Rewind playing music. 362 * @example 363 * //example 364 * cc.AudioEngine.getInstance().rewindMusic(); 365 */ 366 rewindMusic:function () { 367 if (this._soundList[this._playingMusic]) { 368 var au = this._soundList[this._playingMusic].audio; 369 au.currentTime = 0; 370 au.play(); 371 au.addEventListener("pause", this._musicListenerBound , false); 372 cc.AudioEngine.isMusicPlaying = true; 373 this._musicIsStopped = false; 374 } 375 }, 376 377 /** 378 * The volume of the music max value is 1.0,the min value is 0.0 . 379 * @return {Number} 380 * @example 381 * //example 382 * var volume = cc.AudioEngine.getInstance().getMusicVolume(); 383 */ 384 getMusicVolume:function () { 385 if (this._soundList[this._playingMusic]) { 386 return this._soundList[this._playingMusic].audio.volume; 387 } 388 return 0; 389 }, 390 391 /** 392 * Set the volume of music. 393 * @param {Number} volume Volume must be in 0.0~1.0 . 394 * @example 395 * //example 396 * cc.AudioEngine.getInstance().setMusicVolume(0.5); 397 */ 398 setMusicVolume:function (volume) { 399 if (this._soundList[this._playingMusic]) { 400 var music = this._soundList[this._playingMusic].audio; 401 if (volume > 1) { 402 music.volume = 1; 403 } else if (volume < 0) { 404 music.volume = 0; 405 } else { 406 music.volume = volume; 407 } 408 } 409 }, 410 411 /** 412 * Whether the music is playing. 413 * @return {Boolean} If is playing return true,or return false. 414 * @example 415 * //example 416 * if (cc.AudioEngine.getInstance().isMusicPlaying()) { 417 * cc.log("music is playing"); 418 * } 419 * else { 420 * cc.log("music is not playing"); 421 * } 422 */ 423 isMusicPlaying: function () { 424 return cc.AudioEngine.isMusicPlaying; 425 }, 426 427 /** 428 * Play sound effect. 429 * @param {String} path The path of the sound effect with filename extension. 430 * @param {Boolean} loop Whether to loop the effect playing, default value is false 431 * @return {Number|null} the audio id 432 * @example 433 * //example 434 * var soundId = cc.AudioEngine.getInstance().playEffect(path); 435 */ 436 playEffect: function (path, loop) { 437 if (!this._soundSupported) 438 return null; 439 440 var keyname = this._getPathWithoutExt(path), actExt; 441 if (this._soundList[keyname]) { 442 actExt = this._soundList[keyname].ext; 443 } else { 444 actExt = this._getExtFromFullPath(path); 445 } 446 447 var reclaim = this._getEffectList(keyname), au; 448 if (reclaim.length > 0) { 449 for (var i = 0; i < reclaim.length; i++) { 450 //if one of the effect ended, play it 451 if (reclaim[i].ended) { 452 au = reclaim[i]; 453 au.currentTime = 0; 454 if (window.chrome) 455 au.load(); 456 break; 457 } 458 } 459 } 460 461 if (!au) { 462 if (reclaim.length >= this._maxAudioInstance) { 463 cc.log("Error: " + path + " greater than " + this._maxAudioInstance); 464 return null; 465 } 466 au = new Audio(keyname + "." + actExt); 467 au.volume = this._effectsVolume; 468 reclaim.push(au); 469 } 470 471 if (loop) 472 au.loop = loop; 473 au.play(); 474 var audioID = this._audioID++; 475 this._audioIDList[audioID] = au; 476 return audioID; 477 }, 478 479 /** 480 * Set the volume of sound effects. 481 * @param {Number} volume Volume must be in 0.0~1.0 . 482 * @example 483 * //example 484 * cc.AudioEngine.getInstance().setEffectsVolume(0.5); 485 */ 486 setEffectsVolume:function (volume) { 487 if (volume > 1) 488 this._effectsVolume = 1; 489 else if (volume < 0) 490 this._effectsVolume = 0; 491 else 492 this._effectsVolume = volume; 493 494 var tmpArr, au, locEffectList = this._effectList; 495 for (var key in locEffectList) { 496 tmpArr = locEffectList[key]; 497 if (tmpArr.length > 0) { 498 for (var j = 0; j < tmpArr.length; j++) { 499 au = tmpArr[j]; 500 au.volume = this._effectsVolume; 501 } 502 } 503 } 504 }, 505 506 /** 507 * Pause playing sound effect. 508 * @param {Number} audioID The return value of function playEffect. 509 * @example 510 * //example 511 * cc.AudioEngine.getInstance().pauseEffect(audioID); 512 */ 513 pauseEffect:function (audioID) { 514 if (audioID == null) return; 515 516 if (this._audioIDList[audioID]) { 517 var au = this._audioIDList[audioID]; 518 if (!au.ended) { 519 au.pause(); 520 } 521 } 522 }, 523 524 /** 525 * Pause all playing sound effect. 526 * @example 527 * //example 528 * cc.AudioEngine.getInstance().pauseAllEffects(); 529 */ 530 pauseAllEffects:function () { 531 var tmpArr, au; 532 var locEffectList = this._effectList; 533 for (var i in locEffectList) { 534 tmpArr = locEffectList[i]; 535 for (var j = 0; j < tmpArr.length; j++) { 536 au = tmpArr[j]; 537 if (!au.ended) 538 au.pause(); 539 } 540 } 541 }, 542 543 /** 544 * Resume playing sound effect. 545 * @param {Number} audioID The return value of function playEffect. 546 * @audioID 547 * //example 548 * cc.AudioEngine.getInstance().resumeEffect(audioID); 549 */ 550 resumeEffect:function (audioID) { 551 if (audioID == null) return; 552 553 if (this._audioIDList[audioID]) { 554 var au = this._audioIDList[audioID]; 555 if (!au.ended) 556 au.play(); 557 } 558 }, 559 560 /** 561 * Resume all playing sound effect 562 * @example 563 * //example 564 * cc.AudioEngine.getInstance().resumeAllEffects(); 565 */ 566 resumeAllEffects:function () { 567 var tmpArr, au; 568 var locEffectList = this._effectList; 569 for (var i in locEffectList) { 570 tmpArr = locEffectList[i]; 571 if (tmpArr.length > 0) { 572 for (var j = 0; j < tmpArr.length; j++) { 573 au = tmpArr[j]; 574 if (!au.ended) 575 au.play(); 576 } 577 } 578 } 579 }, 580 581 /** 582 * Stop playing sound effect. 583 * @param {Number} audioID The return value of function playEffect. 584 * @example 585 * //example 586 * cc.AudioEngine.getInstance().stopEffect(audioID); 587 */ 588 stopEffect:function (audioID) { 589 if (audioID == null) return; 590 591 if (this._audioIDList[audioID]) { 592 var au = this._audioIDList[audioID]; 593 if (!au.ended) { 594 au.loop = false; 595 au.duration && (au.currentTime = au.duration); 596 } 597 } 598 }, 599 600 /** 601 * Stop all playing sound effects. 602 * @example 603 * //example 604 * cc.AudioEngine.getInstance().stopAllEffects(); 605 */ 606 stopAllEffects:function () { 607 var tmpArr, au, locEffectList = this._effectList; 608 for (var i in locEffectList) { 609 tmpArr = locEffectList[i]; 610 for (var j = 0; j < tmpArr.length; j++) { 611 au = tmpArr[j]; 612 if (!au.ended) { 613 au.loop = false; 614 au.duration && (au.currentTime = au.duration); 615 } 616 } 617 } 618 }, 619 620 /** 621 * Unload the preloaded effect from internal buffer 622 * @param {String} path 623 * @example 624 * //example 625 * cc.AudioEngine.getInstance().unloadEffect(EFFECT_FILE); 626 */ 627 unloadEffect:function (path) { 628 if (!path) return; 629 var keyname = this._getPathWithoutExt(path); 630 if (this._effectList[keyname]) { 631 delete this._effectList[keyname]; 632 } 633 634 var au, pathName, locAudioIDList = this._audioIDList; 635 for (var k in locAudioIDList) { 636 au = locAudioIDList[k]; 637 pathName = this._getPathWithoutExt(au.src); 638 if(pathName.indexOf(keyname) > -1){ 639 this.stopEffect(k); 640 delete locAudioIDList[k]; 641 } 642 } 643 }, 644 645 _getEffectList:function (elt) { 646 var locEffectList = this._effectList; 647 if (locEffectList[elt]) { 648 return locEffectList[elt]; 649 } else { 650 locEffectList[elt] = []; 651 return locEffectList[elt]; 652 } 653 }, 654 655 _pausePlaying: function(){ 656 var locPausedPlayings = this._pausedPlayings, locSoundList = this._soundList; 657 var tmpArr, au; 658 if (!this._musicIsStopped && locSoundList[this._playingMusic]) { 659 au = locSoundList[this._playingMusic].audio; 660 if (!au.paused) { 661 au.pause(); 662 cc.AudioEngine.isMusicPlaying = false; 663 locPausedPlayings.push(au); 664 } 665 } 666 667 var locEffectList = this._effectList; 668 for (var selKey in locEffectList) { 669 tmpArr = locEffectList[selKey]; 670 for (var j = 0; j < tmpArr.length; j++) { 671 au = tmpArr[j]; 672 if (!au.ended && !au.paused) { 673 au.pause(); 674 locPausedPlayings.push(au); 675 } 676 } 677 } 678 }, 679 680 _resumePlaying: function(){ 681 var locPausedPlayings = this._pausedPlayings, locSoundList = this._soundList; 682 var tmpArr, au; 683 if (!this._musicIsStopped && locSoundList[this._playingMusic]) { 684 au = locSoundList[this._playingMusic].audio; 685 if (locPausedPlayings.indexOf(au) !== -1) { 686 au.play(); 687 au.addEventListener("pause", this._musicListenerBound, false); 688 cc.AudioEngine.isMusicPlaying = true; 689 } 690 } 691 692 var locEffectList = this._effectList; 693 for (var selKey in locEffectList) { 694 tmpArr = locEffectList[selKey]; 695 for (var j = 0; j < tmpArr.length; j++) { 696 au = tmpArr[j]; 697 if (!au.ended && locPausedPlayings.indexOf(au) !== -1) 698 au.play(); 699 } 700 } 701 locPausedPlayings.length = 0; 702 } 703 }); 704 705 cc.PlayingTask = function(id, audio,isLoop, status){ 706 this.id = id; 707 this.audio = audio; 708 this.isLoop = isLoop || false; 709 this.status = status || cc.PlayingTaskStatus.stop; 710 }; 711 712 cc.PlayingTaskStatus = {playing:1, pause:2, stop:3, waiting:4}; 713 714 cc.SimpleAudioEngineForMobile = cc.SimpleAudioEngine.extend({ 715 _playingList: null, 716 _currentTask:null, 717 _isPauseForList: false, 718 _checkFlag: true, 719 _audioEndedCallbackBound: null, 720 721 ctor:function(){ 722 cc.SimpleAudioEngine.prototype.ctor.call(this); 723 this._maxAudioInstance = 2; 724 this._playingList = []; 725 this._isPauseForList = false; 726 this._checkFlag = true; 727 this._audioEndedCallbackBound = this._audioEndCallback.bind(this); 728 }, 729 730 _stopAllEffectsForList: function(){ 731 var tmpArr, au, locEffectList = this._effectList; 732 for (var i in locEffectList) { 733 tmpArr = locEffectList[i]; 734 for (var j = 0; j < tmpArr.length; j++) { 735 au = tmpArr[j]; 736 if (!au.ended) { 737 au.removeEventListener('ended', this._audioEndedCallbackBound, false); 738 au.loop = false; 739 au.duration && (au.currentTime = au.duration); 740 } 741 } 742 } 743 this._playingList.length = 0; 744 this._currentTask = null; 745 }, 746 747 /** 748 * Play music. 749 * @param {String} path The path of the music file without filename extension. 750 * @param {Boolean} loop Whether the music loop or not. 751 * @example 752 * //example 753 * cc.AudioEngine.getInstance().playMusic(path, false); 754 */ 755 playMusic:function (path, loop) { 756 if (!this._soundSupported) 757 return; 758 759 this._stopAllEffectsForList(); 760 761 var keyname = this._getPathWithoutExt(path); 762 var extName = this._getExtFromFullPath(path); 763 var au; 764 765 var locSoundList = this._soundList; 766 if (locSoundList[this._playingMusic]){ 767 var currMusic = locSoundList[this._playingMusic]; 768 currMusic.audio.removeEventListener("pause",this._musicListenerBound , false) 769 currMusic.audio.pause(); 770 } 771 772 this._playingMusic = keyname; 773 if (locSoundList[this._playingMusic]) 774 au = locSoundList[this._playingMusic].audio; 775 else { 776 var sfxCache = new cc.SimpleSFX(); 777 sfxCache.ext = extName; 778 au = sfxCache.audio = new Audio(path); 779 sfxCache.audio.preload = 'auto'; 780 locSoundList[keyname] = sfxCache; 781 sfxCache.audio.load(); 782 } 783 784 au.addEventListener("pause", this._musicListenerBound , false); 785 au.loop = loop || false; 786 au.play(); 787 cc.AudioEngine.isMusicPlaying = true; 788 this._musicIsStopped = false; 789 }, 790 791 isMusicPlaying:function(){ 792 var locSoundList = this._soundList, locPlayingMusic = this._playingMusic; 793 if (locSoundList[locPlayingMusic]) { 794 var au = locSoundList[locPlayingMusic].audio; 795 return (!au.paused && !au.ended); 796 } 797 return false; 798 }, 799 800 _musicListener:function(){ 801 cc.AudioEngine.isMusicPlaying = false; 802 if (this._soundList[this._playingMusic]) { 803 var au = this._soundList[this._playingMusic].audio; 804 au.removeEventListener('pause', this._musicListener, false); 805 } 806 if(this._checkFlag) 807 this._isPauseForList = false; 808 else 809 this._checkFlag = true; 810 }, 811 812 _stopExpiredTask:function(expendTime){ 813 var locPlayingList = this._playingList, locAudioIDList = this._audioIDList; 814 for(var i = 0; i < locPlayingList.length; ){ 815 var selTask = locPlayingList[i]; 816 if ((selTask.status === cc.PlayingTaskStatus.waiting)){ 817 if (selTask.audio.currentTime + expendTime >= selTask.audio.duration) { 818 locPlayingList.splice(i, 1); 819 if (locAudioIDList[selTask.id]) { 820 var au = locAudioIDList[selTask.id]; 821 if (!au.ended) { 822 au.removeEventListener('ended', this._audioEndedCallbackBound, false); 823 au.loop = false; 824 au.duration && (au.currentTime = au.duration); 825 } 826 } 827 continue; 828 } else 829 selTask.audio.currentTime = selTask.audio.currentTime + expendTime; 830 } 831 i++; 832 } 833 }, 834 835 _audioEndCallback: function () { 836 var locCurrentTask = this._currentTask; 837 var expendTime = locCurrentTask.audio.currentTime; 838 this._stopExpiredTask(expendTime); 839 840 if (locCurrentTask.isLoop) { 841 locCurrentTask.audio.play(); 842 return; 843 } 844 845 locCurrentTask.audio.removeEventListener('ended', this._audioEndedCallbackBound, false); 846 cc.ArrayRemoveObject(this._playingList, locCurrentTask); 847 848 locCurrentTask = this._getNextTaskToPlay(); 849 if (!locCurrentTask) { 850 this._currentTask = null; 851 if (this._isPauseForList) { 852 this._isPauseForList = false; 853 this.resumeMusic(); 854 } 855 } else { 856 this._currentTask = locCurrentTask; 857 locCurrentTask.status = cc.PlayingTaskStatus.playing; 858 locCurrentTask.audio.play(); 859 } 860 }, 861 862 _pushingPlayingTask: function(playingTask){ 863 if(!playingTask) 864 throw "cc.SimpleAudioEngineForMobile._pushingPlayingTask(): playingTask should be non-null."; 865 866 var locPlayingTaskList = this._playingList; 867 if(!this._currentTask){ 868 if(this.isMusicPlaying()){ 869 this._checkFlag = false; 870 this.pauseMusic(); 871 this._isPauseForList = true; 872 } 873 }else{ 874 this._currentTask.status = cc.PlayingTaskStatus.waiting; 875 this._currentTask.audio.pause(); 876 } 877 locPlayingTaskList.push(playingTask); 878 this._currentTask = playingTask; 879 this._playingAudioTask(playingTask) 880 }, 881 882 _playingAudioTask: function(playTask){ 883 playTask.audio.addEventListener("ended", this._audioEndedCallbackBound, false); 884 playTask.audio.play(); 885 playTask.status = cc.PlayingTaskStatus.playing; 886 }, 887 888 _getPlayingTaskFromList:function(audioID){ 889 var locPlayList = this._playingList; 890 for(var i = 0, len = locPlayList.length;i< len;i++){ 891 if(locPlayList[i].id === audioID) 892 return locPlayList[i]; 893 } 894 return null; 895 }, 896 897 _getNextTaskToPlay: function(){ 898 var locPlayingList = this._playingList; 899 for(var i = locPlayingList.length -1; i >= 0; i--){ 900 var selTask = locPlayingList[i]; 901 if(selTask.status === cc.PlayingTaskStatus.waiting) 902 return selTask; 903 } 904 return null; 905 }, 906 907 _playingNextTask:function(){ 908 var locCurrentTask = this._currentTask = this._getNextTaskToPlay(); 909 if(locCurrentTask){ 910 locCurrentTask.status = cc.PlayingTaskStatus.playing; 911 locCurrentTask.audio.play(); 912 } else { 913 if(this._isPauseForList){ 914 this._isPauseForList = false; 915 this.resumeMusic(); 916 } 917 } 918 }, 919 920 _deletePlayingTaskFromList: function(audioID){ 921 var locPlayList = this._playingList; 922 for(var i = 0, len = locPlayList.length;i< len;i++){ 923 var selTask = locPlayList[i]; 924 if(selTask.id === audioID){ 925 locPlayList.splice(i,1); 926 if(selTask == this._currentTask) 927 this._playingNextTask(); 928 return; 929 } 930 } 931 }, 932 933 _pausePlayingTaskFromList: function (audioID) { 934 var locPlayList = this._playingList; 935 for (var i = 0, len = locPlayList.length; i < len; i++) { 936 var selTask = locPlayList[i]; 937 if (selTask.id === audioID) { 938 selTask.status = cc.PlayingTaskStatus.pause; 939 if (selTask == this._currentTask) 940 this._playingNextTask(); 941 return; 942 } 943 } 944 }, 945 946 _resumePlayingTaskFromList: function(audioID){ 947 var locPlayList = this._playingList; 948 for (var i = 0, len = locPlayList.length; i < len; i++) { 949 var selTask = locPlayList[i]; 950 if (selTask.id === audioID) { 951 selTask.status = cc.PlayingTaskStatus.waiting; 952 if(!this._currentTask){ 953 var locCurrentTask = this._getNextTaskToPlay(); 954 if(locCurrentTask){ 955 //pause music 956 if(this.isMusicPlaying()){ 957 this._checkFlag = false; 958 this.pauseMusic(); 959 this._isPauseForList = true; 960 } 961 locCurrentTask.status = cc.PlayingTaskStatus.playing; 962 locCurrentTask.audio.play(); 963 } 964 } 965 return; 966 } 967 } 968 }, 969 970 /** 971 * Play sound effect. 972 * @param {String} path The path of the sound effect with filename extension. 973 * @param {Boolean} loop Whether to loop the effect playing, default value is false 974 * @return {Number|null} the audio id 975 * @example 976 * //example 977 * var soundId = cc.AudioEngine.getInstance().playEffect(path); 978 */ 979 playEffect: function (path, loop) { 980 if (!this._soundSupported) 981 return null; 982 983 var keyname = this._getPathWithoutExt(path), actExt; 984 if (this._soundList[keyname]) 985 actExt = this._soundList[keyname].ext; 986 else 987 actExt = this._getExtFromFullPath(path); 988 989 var reclaim = this._getEffectList(keyname), au; 990 if (reclaim.length > 0) { 991 for (var i = 0; i < reclaim.length; i++) { 992 //if one of the effect ended, play it 993 if (reclaim[i].ended) { 994 au = reclaim[i]; 995 au.currentTime = 0; 996 if (window.chrome) 997 au.load(); 998 break; 999 } 1000 } 1001 } 1002 1003 if (!au) { 1004 if (reclaim.length >= this._maxAudioInstance) { 1005 cc.log("Error: " + path + " greater than " + this._maxAudioInstance); 1006 return null; 1007 } 1008 au = new Audio(keyname + "." + actExt); 1009 au.volume = this._effectsVolume; 1010 reclaim.push(au); 1011 } 1012 1013 var playingTask = new cc.PlayingTask(this._audioID++, au, loop); 1014 this._pushingPlayingTask(playingTask); 1015 this._audioIDList[playingTask.id] = au; 1016 return playingTask.id; 1017 }, 1018 1019 /** 1020 * Pause playing sound effect. 1021 * @param {Number} audioID The return value of function playEffect. 1022 * @example 1023 * //example 1024 * cc.AudioEngine.getInstance().pauseEffect(audioID); 1025 */ 1026 pauseEffect:function (audioID) { 1027 if (audioID == null) return; 1028 1029 var strID = audioID.toString(); 1030 if (this._audioIDList[strID]) { 1031 var au = this._audioIDList[strID]; 1032 if (!au.ended) au.pause(); 1033 } 1034 this._pausePlayingTaskFromList(audioID); 1035 }, 1036 1037 /** 1038 * Pause all playing sound effect. 1039 * @example 1040 * //example 1041 * cc.AudioEngine.getInstance().pauseAllEffects(); 1042 */ 1043 pauseAllEffects:function () { 1044 var tmpArr, au; 1045 var locEffectList = this._effectList; 1046 for (var selKey in locEffectList) { 1047 tmpArr = locEffectList[selKey]; 1048 for (var j = 0; j < tmpArr.length; j++) { 1049 au = tmpArr[j]; 1050 if (!au.ended) au.pause(); 1051 } 1052 } 1053 1054 var locPlayTask = this._playingList; 1055 for(var i = 0, len = locPlayTask.length; i < len; i++) 1056 locPlayTask[i].status = cc.PlayingTaskStatus.pause; 1057 this._currentTask = null; 1058 1059 if(this._isPauseForList){ 1060 this._isPauseForList = false; 1061 this.resumeMusic(); 1062 } 1063 }, 1064 1065 /** 1066 * Resume playing sound effect. 1067 * @param {Number} audioID The return value of function playEffect. 1068 * @audioID 1069 * //example 1070 * cc.AudioEngine.getInstance().resumeEffect(audioID); 1071 */ 1072 resumeEffect:function (audioID) { 1073 if (audioID == null) return; 1074 1075 if (this._audioIDList[audioID]) { 1076 var au = this._audioIDList[audioID]; 1077 if (!au.ended) 1078 au.play(); 1079 } 1080 this._resumePlayingTaskFromList(audioID); 1081 }, 1082 1083 /** 1084 * Resume all playing sound effect 1085 * @example 1086 * //example 1087 * cc.AudioEngine.getInstance().resumeAllEffects(); 1088 */ 1089 resumeAllEffects:function () { 1090 var tmpArr, au; 1091 var locEffectList = this._effectList; 1092 for (var selKey in locEffectList) { 1093 tmpArr = locEffectList[selKey]; 1094 if (tmpArr.length > 0) { 1095 for (var j = 0; j < tmpArr.length; j++) { 1096 au = tmpArr[j]; 1097 if (!au.ended) au.play(); 1098 } 1099 } 1100 } 1101 1102 var locPlayingList = this._playingList; 1103 for(var i = 0, len = locPlayingList.length; i < len; i++){ 1104 var selTask = locPlayingList[i]; 1105 if(selTask.status === cc.PlayingTaskStatus.pause) 1106 selTask.status = cc.PlayingTaskStatus.waiting; 1107 } 1108 if(this._currentTask == null){ 1109 var locCurrentTask = this._getNextTaskToPlay(); 1110 if(locCurrentTask){ 1111 //pause music 1112 if(this.isMusicPlaying()){ 1113 this._checkFlag = false; 1114 this.pauseMusic(); 1115 this._isPauseForList = true; 1116 } 1117 locCurrentTask.status = cc.PlayingTaskStatus.playing; 1118 locCurrentTask.audio.play(); 1119 } 1120 } 1121 }, 1122 1123 /** 1124 * Stop playing sound effect. 1125 * @param {Number} audioID The return value of function playEffect. 1126 * @example 1127 * //example 1128 * cc.AudioEngine.getInstance().stopEffect(audioID); 1129 */ 1130 stopEffect:function (audioID) { 1131 if (audioID == null) return; 1132 1133 if (this._audioIDList[audioID]) { 1134 var au = this._audioIDList[audioID]; 1135 if (!au.ended) { 1136 au.removeEventListener('ended', this._audioEndedCallbackBound, false); 1137 au.loop = false; 1138 au.duration && (au.currentTime = au.duration); 1139 } 1140 } 1141 this._deletePlayingTaskFromList(audioID); 1142 }, 1143 1144 /** 1145 * Stop all playing sound effects. 1146 * @example 1147 * //example 1148 * cc.AudioEngine.getInstance().stopAllEffects(); 1149 */ 1150 stopAllEffects:function () { 1151 var tmpArr, au, locEffectList = this._effectList; 1152 for (var i in locEffectList) { 1153 tmpArr = locEffectList[i]; 1154 for (var j = 0; j < tmpArr.length; j++) { 1155 au = tmpArr[j]; 1156 if (!au.ended) { 1157 au.removeEventListener('ended', this._audioEndedCallbackBound, false); 1158 au.loop = false; 1159 au.duration && (au.currentTime = au.duration); 1160 } 1161 } 1162 } 1163 1164 this._playingList.length = 0; 1165 this._currentTask = null; 1166 1167 if(this._isPauseForList){ 1168 this._isPauseForList = false; 1169 this.resumeMusic(); 1170 } 1171 }, 1172 1173 _pausePlaying: function(){ 1174 var locPausedPlayings = this._pausedPlayings, locSoundList = this._soundList, au; 1175 if (!this._musicIsStopped && locSoundList[this._playingMusic]) { 1176 au = locSoundList[this._playingMusic].audio; 1177 if (!au.paused) { 1178 au.pause(); 1179 cc.AudioEngine.isMusicPlaying = false; 1180 locPausedPlayings.push(au); 1181 } 1182 } 1183 this.stopAllEffects(); 1184 }, 1185 1186 _resumePlaying: function(){ 1187 var locPausedPlayings = this._pausedPlayings, locSoundList = this._soundList, au; 1188 if (!this._musicIsStopped && locSoundList[this._playingMusic]) { 1189 au = locSoundList[this._playingMusic].audio; 1190 if (locPausedPlayings.indexOf(au) !== -1) { 1191 au.play(); 1192 au.addEventListener("pause", this._musicListenerBound, false); 1193 cc.AudioEngine.isMusicPlaying = true; 1194 } 1195 } 1196 locPausedPlayings.length = 0; 1197 } 1198 }); 1199 1200 /** 1201 * The entity stored in cc.WebAudioEngine, representing a sound object 1202 */ 1203 cc.WebAudioSFX = function(key, sourceNode, volumeNode, startTime, pauseTime) { 1204 // the name of the relevant audio resource 1205 this.key = key; 1206 // the node used in Web Audio API in charge of the source data 1207 this.sourceNode = sourceNode; 1208 // the node used in Web Audio API in charge of volume 1209 this.volumeNode = volumeNode; 1210 /* 1211 * when playing started from beginning, startTime is set to the current time of AudioContext.currentTime 1212 * when paused, pauseTime is set to the current time of AudioContext.currentTime 1213 * so how long the music has been played can be calculated 1214 * these won't be used in other cases 1215 */ 1216 this.startTime = startTime || 0; 1217 this.pauseTime = pauseTime || 0; 1218 // by only sourceNode's playbackState, it cannot distinguish finished state from paused state 1219 this.isPaused = false; 1220 }; 1221 1222 /** 1223 * The Audio Engine implementation via Web Audio API. 1224 * @class 1225 * @extends cc.AudioEngine 1226 */ 1227 cc.WebAudioEngine = cc.AudioEngine.extend(/** @lends cc.WebAudioEngine# */{ 1228 // the Web Audio Context 1229 _ctx: null, 1230 // containing all binary buffers of loaded audio resources 1231 _audioData: null, 1232 /* 1233 * Issue: When loading two resources with different suffixes asynchronously, the second one might start loading 1234 * when the first one is already loading! 1235 * To avoid this duplication, loading synchronously somehow doesn't work. _ctx.decodeAudioData() would throw an 1236 * exception "DOM exception 12", it should be a bug of the browser. 1237 * So just add something to mark some audios as LOADING so as to avoid duplication. 1238 */ 1239 _audiosLoading: null, 1240 // the volume applied to the music 1241 _musicVolume: 1, 1242 // the effects being played: { key => [cc.WebAudioSFX] }, many effects of the same resource may be played simultaneously 1243 _effects: null, 1244 1245 /* 1246 * _canPlay is a property in cc.SimpleAudioEngine, but not used in cc.WebAudioEngine. 1247 * Only those which support Web Audio API will be using this cc.WebAudioEngine, so no need to add an extra check. 1248 */ 1249 // _canPlay: true, 1250 /* 1251 * _maxAudioInstance is also a property in cc.SimpleAudioEngine, but not used here 1252 */ 1253 // _maxAudioInstance: 10, 1254 1255 /** 1256 * Constructor 1257 */ 1258 ctor: function() { 1259 cc.AudioEngine.prototype.ctor.call(this); 1260 this._audioData = {}; 1261 this._audiosLoading = {}; 1262 this._effects = {}; 1263 }, 1264 1265 /** 1266 * Initialization 1267 * @return {Boolean} 1268 */ 1269 init: function() { 1270 /* 1271 * browser has proved to support Web Audio API in miniFramework.js 1272 * only in that case will cc.WebAudioEngine be chosen to run, thus the following is guaranteed to work 1273 */ 1274 this._ctx = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext)(); 1275 1276 // gather capabilities information, enable sound if any of the audio format is supported 1277 var capabilities = {}; 1278 this._checkCanPlay(capabilities); 1279 1280 var formats = ["ogg", "mp3", "wav", "mp4", "m4a"], locSupportedFormat = this._supportedFormat; 1281 for (var idx in formats) { 1282 var name = formats[idx]; 1283 if (capabilities[name]) 1284 locSupportedFormat.push(name); 1285 } 1286 this._soundSupported = locSupportedFormat.length > 0; 1287 return this._soundSupported; 1288 }, 1289 1290 /** 1291 * Using XMLHttpRequest to retrieve the resource data from server. 1292 * Not using cc.FileUtils.getByteArrayFromFile() because it is synchronous, 1293 * so doing the retrieving here is more handful. 1294 * @param {String} url The url to retrieve data 1295 * @param {Object} onSuccess The callback to run when retrieving succeeds, the binary data array is passed into it 1296 * @param {Object} onError The callback to run when retrieving fails 1297 * @private 1298 */ 1299 _fetchData: function(url, onSuccess, onError) { 1300 // currently, only the webkit browsers support Web Audio API, so it should be fine just writing like this. 1301 var req = new window.XMLHttpRequest(); 1302 var realPath = this._resPath + url; 1303 req.open('GET', realPath, true); 1304 req.responseType = 'arraybuffer'; 1305 var engine = this; 1306 req.onload = function() { 1307 // when context decodes the array buffer successfully, call onSuccess 1308 engine._ctx.decodeAudioData(req.response, onSuccess, onError); 1309 }; 1310 req.onerror = onError; 1311 req.send(); 1312 }, 1313 1314 /** 1315 * Preload music resource.<br /> 1316 * This method is called when cc.Loader preload resources. 1317 * @param {String} path The path of the music file with filename extension. 1318 * @param {Function} selector 1319 * @param {Object} target 1320 */ 1321 preloadSound: function(path, selector, target) { 1322 if (!this._soundSupported) 1323 return; 1324 1325 var extName = this._getExtFromFullPath(path); 1326 var keyName = this._getPathWithoutExt(path); 1327 1328 // not supported, already loaded, already loading 1329 if (this._audioData[keyName] || this._audiosLoading[keyName] || !this.isFormatSupported(extName)) { 1330 cc.doCallback(selector, target); 1331 return; 1332 } 1333 1334 this._audiosLoading[keyName] = true; 1335 var engine = this; 1336 this._fetchData(path, function(buffer) { 1337 // resource fetched, in @param buffer 1338 engine._audioData[keyName] = buffer; 1339 delete engine._audiosLoading[keyName]; 1340 cc.doCallback(selector, target); 1341 }, function() { 1342 // resource fetching failed 1343 delete engine._audiosLoading[keyName]; 1344 cc.doCallback(selector, target, path); 1345 }); 1346 }, 1347 1348 /** 1349 * Init a new WebAudioSFX and play it, return this WebAudioSFX object 1350 * assuming that key exists in this._audioData 1351 * @param {String} key 1352 * @param {Boolean} loop Default value is false 1353 * @param {Number} volume 0.0 - 1.0, default value is 1.0 1354 * @param {Number} [offset] Where to start playing (in seconds) 1355 * @private 1356 */ 1357 _beginSound: function(key, loop, volume, offset) { 1358 var sfxCache = new cc.WebAudioSFX(); 1359 loop = loop == null ? false : loop; 1360 volume = volume == null ? 1 : volume; 1361 offset = offset || 0; 1362 1363 var locCtx = this._ctx; 1364 sfxCache.key = key; 1365 sfxCache.sourceNode = this._ctx.createBufferSource(); 1366 sfxCache.sourceNode.buffer = this._audioData[key]; 1367 sfxCache.sourceNode.loop = loop; 1368 if(locCtx.createGain) 1369 sfxCache.volumeNode = this._ctx.createGain(); 1370 else 1371 sfxCache.volumeNode = this._ctx.createGainNode(); 1372 sfxCache.volumeNode.gain.value = volume; 1373 1374 sfxCache.sourceNode.connect(sfxCache.volumeNode); 1375 sfxCache.volumeNode.connect(this._ctx.destination); 1376 1377 /* 1378 * Safari on iOS 6 only supports noteOn(), noteGrainOn(), and noteOff() now.(iOS 6.1.3) 1379 * The latest version of chrome has supported start() and stop() 1380 * start() & stop() are specified in the latest specification (written on 04/26/2013) 1381 * Reference: https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html 1382 * noteOn(), noteGrainOn(), and noteOff() are specified in Draft 13 version (03/13/2012) 1383 * Reference: http://www.w3.org/2011/audio/drafts/2WD/Overview.html 1384 */ 1385 if (sfxCache.sourceNode.start) { 1386 // starting from offset means resuming from where it paused last time 1387 sfxCache.sourceNode.start(0, offset); 1388 } else if (sfxCache.sourceNode.noteGrainOn) { 1389 var duration = sfxCache.sourceNode.buffer.duration; 1390 if (loop) { 1391 /* 1392 * On Safari on iOS 6, if loop == true, the passed in @param duration will be the duration from now on. 1393 * In other words, the sound will keep playing the rest of the music all the time. 1394 * On latest chrome desktop version, the passed in duration will only be the duration in this cycle. 1395 * Now that latest chrome would have start() method, it is prepared for iOS here. 1396 */ 1397 sfxCache.sourceNode.noteGrainOn(0, offset, duration); 1398 } else { 1399 sfxCache.sourceNode.noteGrainOn(0, offset, duration - offset); 1400 } 1401 } else { 1402 // if only noteOn() is supported, resuming sound will NOT work 1403 sfxCache.sourceNode.noteOn(0); 1404 } 1405 1406 // currentTime - offset is necessary for pausing multiple times! 1407 sfxCache.startTime = this._ctx.currentTime - offset; 1408 sfxCache.pauseTime = sfxCache.startTime; 1409 sfxCache.isPaused = false; 1410 1411 return sfxCache; 1412 }, 1413 1414 /** 1415 * <p> 1416 * According to the spec: dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html <br/> 1417 * const unsigned short UNSCHEDULED_STATE = 0; <br/> 1418 * const unsigned short SCHEDULED_STATE = 1; <br/> 1419 * const unsigned short PLAYING_STATE = 2; // this means it is playing <br/> 1420 * const unsigned short FINISHED_STATE = 3; <br/> 1421 * However, the older specification doesn't include this property, such as this one: http://www.w3.org/2011/audio/drafts/2WD/Overview.html 1422 * </p> 1423 * @param {Object} sfxCache Assuming not null 1424 * @returns {Boolean} Whether sfxCache is playing or not 1425 * @private 1426 */ 1427 _isSoundPlaying: function(sfxCache) { 1428 return sfxCache.sourceNode.playbackState == 2; 1429 }, 1430 1431 /** 1432 * To distinguish 3 kinds of status for each sound (PLAYING, PAUSED, FINISHED), _isSoundPlaying() is not enough 1433 * @param {Object} sfxCache Assuming not null 1434 * @returns {Boolean} 1435 * @private 1436 */ 1437 _isSoundPaused: function(sfxCache) { 1438 // checking _isSoundPlaying() won't hurt 1439 return this._isSoundPlaying(sfxCache) ? false : sfxCache.isPaused; 1440 }, 1441 1442 /** 1443 * Whether it is playing any music 1444 * @return {Boolean} If is playing return true,or return false. 1445 * @example 1446 * //example 1447 * if (cc.AudioEngine.getInstance().isMusicPlaying()) { 1448 * cc.log("music is playing"); 1449 * } 1450 * else { 1451 * cc.log("music is not playing"); 1452 * } 1453 */ 1454 isMusicPlaying: function () { 1455 /* 1456 * cc.AudioEngine.isMusicPlaying property is not going to be used here in cc.WebAudioEngine 1457 * that is only used in cc.SimpleAudioEngine 1458 * WebAudioEngine uses Web Audio API which contains a playbackState property in AudioBufferSourceNode 1459 * So there is also no need to be any method like setMusicPlaying(), it is done automatically 1460 */ 1461 return this._playingMusic ? this._isSoundPlaying(this._playingMusic) : false; 1462 }, 1463 1464 /** 1465 * Play music. 1466 * @param {String} path The path of the music file without filename extension. 1467 * @param {Boolean} loop Whether the music loop or not. 1468 * @example 1469 * //example 1470 * cc.AudioEngine.getInstance().playMusic(path, false); 1471 */ 1472 playMusic: function (path, loop) { 1473 var keyName = this._getPathWithoutExt(path); 1474 var extName = this._getExtFromFullPath(path); 1475 loop = loop || false; 1476 1477 if (this._playingMusic) { 1478 // there is a music being played currently, stop it (may be paused) 1479 this.stopMusic(); 1480 } 1481 1482 if (this._audioData[keyName]) { 1483 // already loaded, just play it 1484 this._playingMusic = this._beginSound(keyName, loop, this._musicVolume); 1485 } else if (!this._audiosLoading[keyName] && this.isFormatSupported(extName)) { 1486 // load now only if the type is supported and it is not being loaded currently 1487 this._audiosLoading[keyName] = true; 1488 var engine = this; 1489 this._fetchData(path, function(buffer) { 1490 // resource fetched, save it and call playMusic() again, this time it should be alright 1491 engine._audioData[keyName] = buffer; 1492 delete engine._audiosLoading[keyName]; 1493 engine.playMusic(path, loop); 1494 }, function() { 1495 // resource fetching failed, doing nothing here 1496 delete engine._audiosLoading[keyName]; 1497 /* 1498 * Potential Bug: if fetching data fails every time, loading will be tried again and again. 1499 * Preloading would prevent this issue: if it fails to fetch, preloading procedure will not achieve 100%. 1500 */ 1501 }); 1502 } 1503 }, 1504 1505 /** 1506 * Ends a sound, call stop() or noteOff() accordingly 1507 * @param {Object} sfxCache Assuming not null 1508 * @private 1509 */ 1510 _endSound: function(sfxCache) { 1511 if (sfxCache.sourceNode.playbackState && sfxCache.sourceNode.playbackState == 3) 1512 return; 1513 if (sfxCache.sourceNode.stop) { 1514 sfxCache.sourceNode.stop(0); 1515 } else { 1516 sfxCache.sourceNode.noteOff(0); 1517 } 1518 // Do not call disconnect()! Otherwise the sourceNode's playbackState may not be updated correctly 1519 // sfxCache.sourceNode.disconnect(); 1520 // sfxCache.volumeNode.disconnect(); 1521 }, 1522 1523 /** 1524 * Stop playing music. 1525 * @param {Boolean} [releaseData] If release the music data or not.As default value is false. 1526 * @example 1527 * //example 1528 * cc.AudioEngine.getInstance().stopMusic(); 1529 */ 1530 stopMusic: function(releaseData) { 1531 // can stop when it's playing/paused 1532 var locMusic = this._playingMusic; 1533 if (!locMusic) 1534 return; 1535 1536 var key = locMusic.key; 1537 this._endSound(locMusic); 1538 this._playingMusic = null; 1539 1540 if (releaseData) 1541 delete this._audioData[key]; 1542 }, 1543 1544 /** 1545 * Used in pauseMusic() & pauseEffect() & pauseAllEffects() 1546 * @param {Object} sfxCache Assuming not null 1547 * @private 1548 */ 1549 _pauseSound: function(sfxCache) { 1550 sfxCache.pauseTime = this._ctx.currentTime; 1551 sfxCache.isPaused = true; 1552 this._endSound(sfxCache); 1553 }, 1554 1555 /** 1556 * Pause playing music. 1557 * @example 1558 * //example 1559 * cc.AudioEngine.getInstance().pauseMusic(); 1560 */ 1561 pauseMusic: function() { 1562 // can pause only when it's playing 1563 if (!this.isMusicPlaying()) 1564 return; 1565 this._pauseSound(this._playingMusic); 1566 }, 1567 1568 /** 1569 * Used in resumeMusic() & resumeEffect() & resumeAllEffects() 1570 * @param {Object} paused The paused WebAudioSFX, assuming not null 1571 * @param {Number} volume Can be getMusicVolume() or getEffectsVolume() 1572 * @returns {Object} A new WebAudioSFX object representing the resumed sound 1573 * @private 1574 */ 1575 _resumeSound: function(paused, volume) { 1576 var key = paused.key; 1577 var loop = paused.sourceNode.loop; 1578 // the paused sound may have been playing several loops, (pauseTime - startTime) may be too large 1579 var offset = (paused.pauseTime - paused.startTime) % paused.sourceNode.buffer.duration; 1580 1581 return this._beginSound(key, loop, volume, offset); 1582 }, 1583 1584 /** 1585 * Resume playing music. 1586 * @example 1587 * //example 1588 * cc.AudioEngine.getInstance().resumeMusic(); 1589 */ 1590 resumeMusic: function() { 1591 var locMusic = this._playingMusic; 1592 // can resume only when it's paused 1593 if (!locMusic || !this._isSoundPaused(locMusic)) { 1594 return; 1595 } 1596 this._playingMusic = this._resumeSound(locMusic, this.getMusicVolume()); 1597 }, 1598 1599 /** 1600 * Rewind playing music. 1601 * @example 1602 * //example 1603 * cc.AudioEngine.getInstance().rewindMusic(); 1604 */ 1605 rewindMusic: function() { 1606 var locMusic = this._playingMusic; 1607 // can rewind when it's playing or paused 1608 if (!locMusic) 1609 return; 1610 1611 var key = locMusic.key; 1612 var loop = locMusic.sourceNode.loop; 1613 var volume = this.getMusicVolume(); 1614 1615 this._endSound(locMusic); 1616 this._playingMusic = this._beginSound(key, loop, volume); 1617 }, 1618 1619 /** 1620 * The volume of the music max value is 1.0,the min value is 0.0 . 1621 * @return {Number} 1622 * @example 1623 * //example 1624 * var volume = cc.AudioEngine.getInstance().getMusicVolume(); 1625 */ 1626 getMusicVolume: function() { 1627 return this._musicVolume; 1628 }, 1629 1630 /** 1631 * update volume, used in setMusicVolume() or setEffectsVolume() 1632 * @param {Object} sfxCache Assuming not null 1633 * @param {Number} volume 1634 * @private 1635 */ 1636 _setSoundVolume: function(sfxCache, volume) { 1637 sfxCache.volumeNode.gain.value = volume; 1638 }, 1639 1640 /** 1641 * Set the volume of music. 1642 * @param {Number} volume Volume must be in 0.0~1.0 . 1643 * @example 1644 * //example 1645 * cc.AudioEngine.getInstance().setMusicVolume(0.5); 1646 */ 1647 setMusicVolume: function(volume) { 1648 if (volume > 1) 1649 volume = 1; 1650 else if (volume < 0) 1651 volume = 0; 1652 1653 if (this.getMusicVolume() == volume) // it is the same, no need to update 1654 return; 1655 1656 this._musicVolume = volume; 1657 if (this._playingMusic) 1658 this._setSoundVolume(this._playingMusic, volume); 1659 }, 1660 1661 /** 1662 * Play sound effect. 1663 * @param {String} path The path of the sound effect with filename extension. 1664 * @param {Boolean} loop Whether to loop the effect playing, default value is false 1665 * @return {Number|null} 1666 * @example 1667 * //example 1668 * cc.AudioEngine.getInstance().playEffect(path); 1669 */ 1670 playEffect: function(path, loop) { 1671 var keyName = this._getPathWithoutExt(path), extName = this._getExtFromFullPath(path), audioID; 1672 1673 loop = loop || false; 1674 1675 if (this._audioData[keyName]) { 1676 // the resource has been loaded, just play it 1677 var locEffects = this._effects; 1678 if (!locEffects[keyName]) { 1679 locEffects[keyName] = []; 1680 } 1681 // a list of sound objects from the same resource 1682 var effectList = locEffects[keyName]; 1683 for (var idx = 0, len = effectList.length; idx < len; idx++) { 1684 var sfxCache = effectList[idx]; 1685 if (!this._isSoundPlaying(sfxCache) && !this._isSoundPaused(sfxCache)) { 1686 // not playing && not paused => it is finished, this position can be reused 1687 effectList[idx] = this._beginSound(keyName, loop, this.getEffectsVolume()); 1688 audioID = this._audioID++; 1689 this._audioIDList[audioID] = effectList[idx]; 1690 return audioID; 1691 } 1692 } 1693 // no new sound was created to replace an old one in the list, then just append one 1694 var addSFX = this._beginSound(keyName, loop, this.getEffectsVolume()); 1695 effectList.push(addSFX); 1696 audioID = this._audioID++; 1697 this._audioIDList[audioID] = addSFX; 1698 return audioID; 1699 } else if (!this._audiosLoading[keyName] && this.isFormatSupported(extName)) { 1700 // load now only if the type is supported and it is not being loaded currently 1701 this._audiosLoading[keyName] = true; 1702 var engine = this; 1703 audioID = this._audioID++; 1704 this._audioIDList[audioID] = null; 1705 this._fetchData(path, function(buffer) { 1706 // resource fetched, save it and call playEffect() again, this time it should be alright 1707 engine._audioData[keyName] = buffer; 1708 delete engine._audiosLoading[keyName]; 1709 var asynSFX = engine._beginSound(keyName, loop, engine.getEffectsVolume()); 1710 engine._audioIDList[audioID] = asynSFX; 1711 var locEffects = engine._effects; 1712 if (!locEffects[keyName]) 1713 locEffects[keyName] = []; 1714 locEffects[keyName].push(asynSFX); 1715 }, function() { 1716 // resource fetching failed, doing nothing here 1717 delete engine._audiosLoading[keyName]; 1718 delete engine._audioIDList[audioID]; 1719 /* 1720 * Potential Bug: if fetching data fails every time, loading will be tried again and again. 1721 * Preloading would prevent this issue: if it fails to fetch, preloading procedure will not achieve 100%. 1722 */ 1723 }); 1724 return audioID; 1725 } 1726 return null; 1727 }, 1728 1729 /** 1730 * Set the volume of sound effects. 1731 * @param {Number} volume Volume must be in 0.0~1.0 . 1732 * @example 1733 * //example 1734 * cc.AudioEngine.getInstance().setEffectsVolume(0.5); 1735 */ 1736 setEffectsVolume: function(volume) { 1737 if (volume > 1) 1738 volume = 1; 1739 else if (volume < 0) 1740 volume = 0; 1741 1742 if (this._effectsVolume == volume) { 1743 // it is the same, no need to update 1744 return; 1745 } 1746 1747 this._effectsVolume = volume; 1748 var locEffects = this._effects; 1749 for (var key in locEffects) { 1750 var effectList = locEffects[key]; 1751 for (var idx = 0, len = effectList.length; idx < len; idx++) 1752 this._setSoundVolume(effectList[idx], volume); 1753 } 1754 }, 1755 1756 /** 1757 * Used in pauseEffect() and pauseAllEffects() 1758 * @param {Array} effectList A list of sounds, each sound may be playing/paused/finished 1759 * @private 1760 */ 1761 _pauseSoundList: function(effectList) { 1762 for (var idx = 0, len = effectList.length; idx < len; idx++) { 1763 var sfxCache = effectList[idx]; 1764 if (sfxCache && this._isSoundPlaying(sfxCache)) 1765 this._pauseSound(sfxCache); 1766 } 1767 }, 1768 1769 /** 1770 * Pause playing sound effect. 1771 * @param {Number} audioID The return value of function playEffect. 1772 * @example 1773 * //example 1774 * cc.AudioEngine.getInstance().pauseEffect(audioID); 1775 */ 1776 pauseEffect: function(audioID) { 1777 if (audioID == null) 1778 return; 1779 1780 if (this._audioIDList[audioID]){ 1781 var sfxCache = this._audioIDList[audioID]; 1782 if (sfxCache && this._isSoundPlaying(sfxCache)) 1783 this._pauseSound(sfxCache); 1784 } 1785 }, 1786 1787 /** 1788 * Pause all playing sound effect. 1789 * @example 1790 * //example 1791 * cc.AudioEngine.getInstance().pauseAllEffects(); 1792 */ 1793 pauseAllEffects: function() { 1794 for (var key in this._effects) { 1795 this._pauseSoundList(this._effects[key]); 1796 } 1797 }, 1798 1799 /** 1800 * Used in resumeEffect() and resumeAllEffects() 1801 * @param {Array} effectList A list of sounds, each sound may be playing/paused/finished 1802 * @param {Number} volume 1803 * @private 1804 */ 1805 _resumeSoundList: function(effectList, volume) { 1806 for (var idx = 0, len = effectList.length; idx < len; idx++) { 1807 var sfxCache = effectList[idx]; 1808 if (this._isSoundPaused(sfxCache)) { 1809 effectList[idx] = this._resumeSound(sfxCache, volume); 1810 this._updateEffectsList(sfxCache, effectList[idx]); 1811 } 1812 } 1813 }, 1814 1815 /** 1816 * Resume playing sound effect. 1817 * @param {Number} audioID The return value of function playEffect. 1818 * @example 1819 * //example 1820 * cc.AudioEngine.getInstance().resumeEffect(audioID); 1821 */ 1822 resumeEffect: function(audioID) { 1823 if (audioID == null) 1824 return; 1825 1826 if (this._audioIDList[audioID]){ 1827 var sfxCache = this._audioIDList[audioID]; 1828 if (sfxCache && this._isSoundPaused(sfxCache)){ 1829 this._audioIDList[audioID] = this._resumeSound(sfxCache, this.getEffectsVolume()); 1830 this._updateEffectsList(sfxCache, this._audioIDList[audioID]); 1831 } 1832 } 1833 }, 1834 1835 _updateEffectsList:function(oldSFX, newSFX){ 1836 var locEffects = this._effects, locEffectList; 1837 for(var eKey in locEffects){ 1838 locEffectList = locEffects[eKey]; 1839 for(var i = 0; i< locEffectList.length; i++){ 1840 if(locEffectList[i] == oldSFX) 1841 locEffectList[i] = newSFX; 1842 } 1843 } 1844 }, 1845 1846 /** 1847 * Resume all playing sound effect 1848 * @example 1849 * //example 1850 * cc.AudioEngine.getInstance().resumeAllEffects(); 1851 */ 1852 resumeAllEffects: function() { 1853 var locEffects = this._effects; 1854 for (var key in locEffects) 1855 this._resumeSoundList(locEffects[key], this.getEffectsVolume()); 1856 }, 1857 1858 /** 1859 * Stop playing sound effect. 1860 * @param {Number} audioID The return value of function playEffect. 1861 * @example 1862 * //example 1863 * cc.AudioEngine.getInstance().stopEffect(audioID); 1864 */ 1865 stopEffect: function(audioID) { 1866 if (audioID == null) 1867 return; 1868 1869 var locAudioIDList = this._audioIDList; 1870 if (locAudioIDList[audioID]) 1871 this._endSound(locAudioIDList[audioID]); 1872 }, 1873 1874 /** 1875 * Stop all playing sound effects. 1876 * @example 1877 * //example 1878 * cc.AudioEngine.getInstance().stopAllEffects(); 1879 */ 1880 stopAllEffects: function() { 1881 var locEffects = this._effects; 1882 for (var key in locEffects) { 1883 var effectList = locEffects[key]; 1884 for (var idx = 0, len = effectList.length; idx < len; idx++) 1885 this._endSound(effectList[idx]); 1886 /* 1887 * Another way is to set this._effects = {} outside this for loop. 1888 * However, the cc.Class.extend() put all properties in the prototype. 1889 * If I reassign a new {} to it, that will be appear in the instance. 1890 * In other words, the dict in prototype won't release its children. 1891 */ 1892 delete locEffects[key]; 1893 } 1894 }, 1895 1896 /** 1897 * Unload the preloaded effect from internal buffer 1898 * @param {String} path 1899 * @example 1900 * //example 1901 * cc.AudioEngine.getInstance().unloadEffect(EFFECT_FILE); 1902 */ 1903 unloadEffect: function(path) { 1904 if (!path) 1905 return; 1906 1907 var keyName = this._getPathWithoutExt(path); 1908 if (this._effects[keyName]){ 1909 var locEffect = this._effects[keyName]; 1910 delete this._effects[keyName]; 1911 var locAudioIDList = this._audioIDList; 1912 for(var auID in locAudioIDList){ 1913 if(locEffect.indexOf(locAudioIDList[auID]) > -1){ 1914 this.stopEffect(auID); 1915 delete locAudioIDList[auID]; 1916 } 1917 } 1918 } 1919 1920 if (this._audioData[keyName]) 1921 delete this._audioData[keyName]; 1922 }, 1923 1924 _pausePlaying: function(){ 1925 var locPausedPlayings = this._pausedPlayings; 1926 if (this.isMusicPlaying()){ 1927 locPausedPlayings.push(this._playingMusic); 1928 this._pauseSound(this._playingMusic); 1929 } 1930 1931 var locEffects = this._effects; 1932 for (var selKey in locEffects) { 1933 var selEffectList = locEffects[selKey]; 1934 for (var idx = 0, len = selEffectList.length; idx < len; idx++) { 1935 var sfxCache = selEffectList[idx]; 1936 if (sfxCache && this._isSoundPlaying(sfxCache)) { 1937 locPausedPlayings.push(sfxCache); 1938 this._pauseSound(sfxCache); 1939 } 1940 } 1941 } 1942 }, 1943 1944 _resumePlaying: function(){ 1945 var locPausedPlayings = this._pausedPlayings, locVolume = this.getMusicVolume(); 1946 1947 var locMusic = this._playingMusic; 1948 // can resume only when it's paused 1949 if (locMusic && this._isSoundPaused(locMusic) && locPausedPlayings.indexOf(locMusic) != -1) 1950 this._playingMusic = this._resumeSound(locMusic, locVolume); 1951 1952 var locEffects = this._effects; 1953 for (var selKey in locEffects){ 1954 var selEffects = locEffects[selKey]; 1955 for (var idx = 0, len = selEffects.length; idx < len; idx++) { 1956 var sfxCache = selEffects[idx]; 1957 if (this._isSoundPaused(sfxCache) &&locPausedPlayings.indexOf(sfxCache) != -1) { 1958 selEffects[idx] = this._resumeSound(sfxCache, locVolume); 1959 this._updateEffectsList(sfxCache, selEffects[idx]); 1960 } 1961 } 1962 } 1963 locPausedPlayings.length = 0; 1964 } 1965 }); 1966 1967 cc.AudioEngine._instance = null; 1968 1969 cc.AudioEngine.isMusicPlaying = false; 1970 1971 /** 1972 * Get the shared Engine object, it will new one when first time be called. 1973 * @return {cc.AudioEngine} 1974 */ 1975 cc.AudioEngine.getInstance = function () { 1976 if (!this._instance) { 1977 if (cc.Browser.supportWebAudio) { 1978 this._instance = new cc.WebAudioEngine(); 1979 } else { 1980 if (cc.Browser.multipleAudioWhiteList.indexOf(cc.Browser.type) !== -1) 1981 this._instance = new cc.SimpleAudioEngine(); 1982 else 1983 this._instance = new cc.SimpleAudioEngineForMobile(); 1984 } 1985 this._instance.init(); 1986 } 1987 return this._instance; 1988 }; 1989 1990 /** 1991 * Stop all music and sound effects 1992 * @example 1993 * //example 1994 * cc.AudioEngine.end(); 1995 */ 1996 cc.AudioEngine.end = function () { 1997 if (this._instance) { 1998 this._instance.stopMusic(); 1999 this._instance.stopAllEffects(); 2000 } 2001 this._instance = null; 2002 }; 2003