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