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