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