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