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/Hilo
 13  * @requires hilo/core/Class
 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 AudioContext = window.AudioContext || window.webkitAudioContext;
 27 var context = AudioContext ? new AudioContext() : null;
 28 
 29 return Class.create(/** @lends WebAudio.prototype */{
 30     Mixes: EventMixin,
 31     constructor: function(properties){
 32         Hilo.copy(this, properties, true);
 33 
 34         this._init();
 35     },
 36 
 37     src: null,
 38     loop: false,
 39     autoPlay: false,
 40     loaded: false,
 41     playing: false,
 42     duration: 0,
 43     volume: 1,
 44     muted: false,
 45 
 46     _context: null, //WebAudio上下文
 47     _gainNode: null, //音量控制器
 48     _buffer: null, //音频缓冲文件
 49     _audioNode: null, //音频播放器
 50     _startTime: 0, //开始播放时间戳
 51     _offset: 0, //播放偏移量
 52 
 53     /**
 54      * @private 初始化
 55      */
 56     _init:function(){
 57         this._context = context;
 58         this._gainNode = context.createGain ? context.createGain() : context.createGainNode();
 59         this._gainNode.connect(context.destination);
 60 
 61         this._onAudioEvent = this._onAudioEvent.bind(this);
 62         this._onDecodeComplete = this._onDecodeComplete.bind(this);
 63         this._onDecodeError = this._onDecodeError.bind(this);
 64     },
 65     /**
 66      * 加载音频文件。注意:我们使用XMLHttpRequest进行加载,因此需要注意跨域问题。
 67      */
 68     load: function(){
 69         if(!this._buffer){
 70             var request = new XMLHttpRequest();
 71             request.src = this.src;
 72             request.open('GET', this.src, true);
 73             request.responseType = 'arraybuffer';
 74             request.onload = this._onAudioEvent;
 75             request.onprogress = this._onAudioEvent;
 76             request.onerror = this._onAudioEvent;
 77             request.send();
 78             this._buffer = true;
 79         }
 80         return this;
 81     },
 82 
 83     /**
 84      * @private
 85      */
 86     _onAudioEvent: function(e){
 87         // console.log('onAudioEvent:', e.type);
 88         var type = e.type;
 89 
 90         switch(type){
 91             case 'load':
 92                 var request = e.target;
 93                 request.onload = request.onprogress = request.onerror = null;
 94                 this._context.decodeAudioData(request.response, this._onDecodeComplete, this._onDecodeError);
 95                 request = null;
 96                 break;
 97             case 'ended':
 98                 this.playing = false;
 99                 this.fire('end');
100                 if(this.loop) this._doPlay();
101                 break;
102             case 'progress':
103                 this.fire(e);
104                 break;
105             case 'error':
106                 this.fire(e);
107                 break;
108         }
109     },
110 
111     /**
112      * @private
113      */
114     _onDecodeComplete: function(audioBuffer){
115         this._buffer = audioBuffer;
116         this.loaded = true;
117         this.duration = audioBuffer.duration;
118         // console.log('onDecodeComplete:', audioBuffer.duration);
119         this.fire('load');
120         if(this.autoPlay) this._doPlay();
121     },
122 
123     /**
124      * @private
125      */
126     _onDecodeError: function(){
127         this.fire('error');
128     },
129 
130     /**
131      * @private
132      */
133     _doPlay: function(){
134         this._clearAudioNode();
135 
136         var audioNode = this._context.createBufferSource();
137 
138         //some old browser are noteOn/noteOff -> start/stop
139         if(!audioNode.start){
140             audioNode.start = audioNode.noteOn;
141             audioNode.stop = audioNode.noteOff;
142         }
143 
144         audioNode.buffer = this._buffer;
145         audioNode.onended = this._onAudioEvent;
146         this._gainNode.gain.value = this.muted ? 0 : this.volume;
147         audioNode.connect(this._gainNode);
148         audioNode.start(0, this._offset);
149 
150         this._audioNode = audioNode;
151         this._startTime = this._context.currentTime;
152         this.playing = true;
153     },
154 
155     /**
156      * @private
157      */
158     _clearAudioNode: function(){
159         var audioNode = this._audioNode;
160         if(audioNode){
161             audioNode.onended = null;
162             // audioNode.disconnect(this._gainNode);
163             audioNode.disconnect(0);
164             this._audioNode = null;
165         }
166     },
167 
168     /**
169      * 播放音频。如果正在播放,则会重新开始。
170      */
171     play: function(){
172         if(this.playing) this.stop();
173 
174         if(this.loaded){
175             this._doPlay();
176         }else if(!this._buffer){
177             this.autoPlay = true;
178             this.load();
179         }
180 
181         return this;
182     },
183 
184     /**
185      * 暂停音频。
186      */
187     pause: function(){
188         if(this.playing){
189             this._audioNode.stop(0);
190             this._offset += this._context.currentTime - this._startTime;
191             this.playing = false;
192         }
193         return this;
194     },
195 
196     /**
197      * 恢复音频播放。
198      */
199     resume: function(){
200         if(!this.playing){
201             this._doPlay();
202         }
203         return this;
204     },
205 
206     /**
207      * 停止音频播放。
208      */
209     stop: function(){
210         if(this.playing){
211             this._audioNode.stop(0);
212             this._audioNode.disconnect();
213             this._offset = 0;
214             this.playing = false;
215         }
216         return this;
217     },
218 
219     /**
220      * 设置音量。
221      */
222     setVolume: function(volume){
223         if(this.volume != volume){
224             this.volume = volume;
225             this._gainNode.gain.value = volume;
226         }
227         return this;
228     },
229 
230     /**
231      * 设置是否静音。
232      */
233     setMute: function(muted){
234         if(this.muted != muted){
235             this.muted = muted;
236             this._gainNode.gain.value = muted ? 0 : this.volume;
237         }
238         return this;
239     },
240 
241     Statics: /** @lends WebAudio */ {
242         /**
243          * 浏览器是否支持WebAudio。
244          */
245         isSupported: AudioContext != null,
246 
247         /**
248          * 浏览器是否已激活WebAudio。
249          */
250         enabled: false,
251 
252         /**
253          * 激活WebAudio。注意:需用户事件触发此方法才有效。激活后,无需用户事件也可播放音频。
254          */
255         enable: function(){
256             if(!this.enabled && context){
257                 var source = context.createBufferSource();
258                 source.buffer = context.createBuffer(1, 1, 22050);
259                 source.connect(context.destination);
260                 source.start ? source.start(0, 0, 0) : source.noteOn(0, 0, 0);
261                 this.enabled = true;
262                 return true;
263             }
264             return this.enabled;
265         }
266     }
267 });
268 
269 })();