1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 /**
  8  * @class WebAudio声音播放模块。它具有更好的声音播放和控制能力,适合在iOS6+平台使用。
  9  * 兼容情况:iOS6+、Chrome33+、Firefox28+支持,但Android浏览器均不支持。
 10  * @param {Object} properties 创建对象的属性参数。可包含此类所有可写属性。
 11  * @module hilo/media/WebAudio
 12  * @requires hilo/core/Class
 13  * @requires hilo/util/util
 14  * @requires hilo/event/EventMixin
 15  * @property {String} src 播放的音频的资源地址。
 16  * @property {Boolean} loop 是否循环播放。默认为false。
 17  * @property {Boolean} autoPlay 是否自动播放。默认为false。
 18  * @property {Boolean} loaded 音频资源是否已加载完成。只读属性。
 19  * @property {Boolean} playing 是否正在播放音频。只读属性。
 20  * @property {Number} duration 音频的时长。只读属性。
 21  * @property {Number} volume 音量的大小。取值范围:0-1。
 22  * @property {Boolean} muted 是否静音。默认为false。
 23  */
 24 var WebAudio = (function(){
 25 
 26 var context = null;
 27 try {
 28     var AudioContext = window.AudioContext || window.webkitAudioContext;
 29     if (AudioContext) {
 30         context = new AudioContext();
 31     }
 32 } catch(e) {
 33     context = null;
 34 }
 35 
 36 return Class.create(/** @lends WebAudio.prototype */{
 37     Mixes: EventMixin,
 38     constructor: function(properties){
 39         util.copy(this, properties, true);
 40 
 41         this._init();
 42     },
 43 
 44     src: null,
 45     loop: false,
 46     autoPlay: false,
 47     loaded: false,
 48     playing: false,
 49     duration: 0,
 50     volume: 1,
 51     muted: false,
 52 
 53     _context: null, //WebAudio上下文 the WebAudio Context
 54     _gainNode: null, //音量控制器 the volume controller
 55     _buffer: null, //音频缓冲文件 the audio file buffer
 56     _audioNode: null, //音频播放器 the audio playing node
 57     _startTime: 0, //开始播放时间戳 the start time to play the audio
 58     _offset: 0, //播放偏移量 the offset of current playing audio
 59 
 60     /**
 61      * @private 初始化
 62      */
 63     _init:function(){
 64         this._context = context;
 65         this._gainNode = context.createGain ? context.createGain() : context.createGainNode();
 66         this._gainNode.connect(context.destination);
 67 
 68         this._onAudioEvent = this._onAudioEvent.bind(this);
 69         this._onDecodeComplete = this._onDecodeComplete.bind(this);
 70         this._onDecodeError = this._onDecodeError.bind(this);
 71     },
 72     /**
 73      * 加载音频文件。注意:我们使用XMLHttpRequest进行加载,因此需要注意跨域问题。
 74      */
 75     load: function(){
 76         if(!this._buffer){
 77             var buffer = WebAudio._bufferCache[this.src];
 78             if(buffer){
 79                 this._onDecodeComplete(buffer);
 80             }
 81             else{
 82                 var request = new XMLHttpRequest();
 83                 request.src = this.src;
 84                 request.open('GET', this.src, true);
 85                 request.responseType = 'arraybuffer';
 86                 request.onload = this._onAudioEvent;
 87                 request.onprogress = this._onAudioEvent;
 88                 request.onerror = this._onAudioEvent;
 89                 request.send();
 90             }
 91             this._buffer = true;
 92         }
 93         return this;
 94     },
 95 
 96     /**
 97      * @private
 98      */
 99     _onAudioEvent: function(e){
100         // console.log('onAudioEvent:', e.type);
101         var type = e.type;
102 
103         switch(type){
104             case 'load':
105                 var request = e.target;
106                 request.onload = request.onprogress = request.onerror = null;
107                 this._context.decodeAudioData(request.response, this._onDecodeComplete, this._onDecodeError);
108                 request = null;
109                 break;
110             case 'ended':
111                 this.playing = false;
112                 this.fire('end');
113                 if(this.loop) this._doPlay();
114                 break;
115             case 'progress':
116                 this.fire(e);
117                 break;
118             case 'error':
119                 this.fire(e);
120                 break;
121         }
122     },
123 
124     /**
125      * @private
126      */
127     _onDecodeComplete: function(audioBuffer){
128         if(!WebAudio._bufferCache[this.src]){
129             WebAudio._bufferCache[this.src] = audioBuffer;
130         }
131 
132         this._buffer = audioBuffer;
133         this.loaded = true;
134         this.duration = audioBuffer.duration;
135 
136         this.fire('load');
137         if(this.autoPlay) this._doPlay();
138     },
139 
140     /**
141      * @private
142      */
143     _onDecodeError: function(){
144         this.fire('error');
145     },
146 
147     /**
148      * @private
149      */
150     _doPlay: function(){
151         this._clearAudioNode();
152 
153         var audioNode = this._context.createBufferSource();
154 
155         //some old browser are noteOn/noteOff -> start/stop
156         if(!audioNode.start){
157             audioNode.start = audioNode.noteOn;
158             audioNode.stop = audioNode.noteOff;
159         }
160 
161         audioNode.buffer = this._buffer;
162         audioNode.onended = this._onAudioEvent;
163         this._gainNode.gain.value = this.muted ? 0 : this.volume;
164         audioNode.connect(this._gainNode);
165         audioNode.start(0, this._offset);
166 
167         this._audioNode = audioNode;
168         this._startTime = this._context.currentTime;
169         this.playing = true;
170     },
171 
172     /**
173      * @private
174      */
175     _clearAudioNode: function(){
176         var audioNode = this._audioNode;
177         if(audioNode){
178             audioNode.onended = null;
179             // audioNode.disconnect(this._gainNode);
180             audioNode.disconnect(0);
181             this._audioNode = null;
182         }
183     },
184 
185     /**
186      * 播放音频。如果正在播放,则会重新开始。
187      */
188     play: function(){
189         if(this.playing) this.stop();
190 
191         if(this.loaded){
192             this._doPlay();
193         }else if(!this._buffer){
194             this.autoPlay = true;
195             this.load();
196         }
197 
198         return this;
199     },
200 
201     /**
202      * 暂停音频。
203      */
204     pause: function(){
205         if(this.playing){
206             this._audioNode.stop(0);
207             this._offset += this._context.currentTime - this._startTime;
208             this.playing = false;
209         }
210         return this;
211     },
212 
213     /**
214      * 恢复音频播放。
215      */
216     resume: function(){
217         if(!this.playing){
218             this._doPlay();
219         }
220         return this;
221     },
222 
223     /**
224      * 停止音频播放。
225      */
226     stop: function(){
227         if(this.playing){
228             this._audioNode.stop(0);
229             this._audioNode.disconnect();
230             this._offset = 0;
231             this.playing = false;
232         }
233         return this;
234     },
235 
236     /**
237      * 设置音量。
238      */
239     setVolume: function(volume){
240         if(this.volume != volume){
241             this.volume = volume;
242             this._gainNode.gain.value = volume;
243         }
244         return this;
245     },
246 
247     /**
248      * 设置是否静音。
249      */
250     setMute: function(muted){
251         if(this.muted != muted){
252             this.muted = muted;
253             this._gainNode.gain.value = muted ? 0 : this.volume;
254         }
255         return this;
256     },
257 
258     Statics: /** @lends WebAudio */ {
259         /**
260          * 浏览器是否支持WebAudio。
261          */
262         isSupported: context !== null,
263 
264         /**
265          * 浏览器是否已激活WebAudio。
266          */
267         enabled: false,
268 
269         /**
270          * 激活WebAudio。注意:需用户事件触发此方法才有效。激活后,无需用户事件也可播放音频。
271          */
272         enable: function(){
273             if(!this.enabled && context){
274                 var source = context.createBufferSource();
275                 source.buffer = context.createBuffer(1, 1, 22050);
276                 source.connect(context.destination);
277                 source.start ? source.start(0, 0, 0) : source.noteOn(0, 0, 0);
278                 this.enabled = true;
279                 return true;
280             }
281             return this.enabled;
282         },
283         /**
284          * The audio buffer caches.
285          * @private
286          * @type {Object}
287          */
288         _bufferCache:{},
289         /**
290          * 清除audio buffer 缓存。
291          * @param  {String} url audio的网址,默认清除所有的缓存
292          */
293         clearBufferCache:function(url){
294             if(url){
295                 this._bufferCache[url] = null;
296             }
297             else{
298                 this._bufferCache = {};
299             }
300         }
301     }
302 });
303 
304 })();