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