1 /**
  2  * Hilo
  3  * Copyright 2015 alibaba.com
  4  * Licensed under the MIT License
  5  */
  6 
  7 /**
  8  * @language=en
  9  * @class WebAudio audio playing module. It provides a better way to play and control audio, use on iOS6+ platform.
 10  * Compatibility:iOS6+、Chrome33+、Firefox28+ supported,but all Android browsers do not support.
 11  * @param {Object} properties create object properties, include all writable properties of this class.
 12  * @module hilo/media/WebAudio
 13  * @requires hilo/core/Hilo
 14  * @requires hilo/core/Class
 15  * @requires hilo/event/EventMixin
 16  * @property {String} src The source of the playing audio.
 17  * @property {Boolean} loop Is loop playback, default value is false.
 18  * @property {Boolean} autoPlay Is the audio autoplay, default value is false.
 19  * @property {Boolean} loaded Is the audio resource loaded, readonly!
 20  * @property {Boolean} playing Is the audio playing, readonly!
 21  * @property {Number} duration The duration of the audio, readonly!
 22  * @property {Number} volume The volume of the audio, value between 0 to 1.
 23  * @property {Boolean} muted Is the audio muted, default value is false.
 24  */
 25 var WebAudio = (function(){
 26 
 27 var AudioContext = window.AudioContext || window.webkitAudioContext;
 28 var context = AudioContext ? new AudioContext() : null;
 29 
 30 return Class.create(/** @lends WebAudio.prototype */{
 31     Mixes: EventMixin,
 32     constructor: function(properties){
 33         Hilo.copy(this, properties, true);
 34 
 35         this._init();
 36     },
 37 
 38     src: null,
 39     loop: false,
 40     autoPlay: false,
 41     loaded: false,
 42     playing: false,
 43     duration: 0,
 44     volume: 1,
 45     muted: false,
 46 
 47     _context: null, //WebAudio上下文 the WebAudio Context
 48     _gainNode: null, //音量控制器 the volume controller
 49     _buffer: null, //音频缓冲文件 the audio file buffer
 50     _audioNode: null, //音频播放器 the audio playing node
 51     _startTime: 0, //开始播放时间戳 the start time to play the audio
 52     _offset: 0, //播放偏移量 the offset of current playing audio
 53 
 54     /**
 55      * @language=en
 56      * @private Initialize.
 57      */
 58     _init:function(){
 59         this._context = context;
 60         this._gainNode = context.createGain ? context.createGain() : context.createGainNode();
 61         this._gainNode.connect(context.destination);
 62 
 63         this._onAudioEvent = this._onAudioEvent.bind(this);
 64         this._onDecodeComplete = this._onDecodeComplete.bind(this);
 65         this._onDecodeError = this._onDecodeError.bind(this);
 66     },
 67     /**
 68      * @language=en
 69      * Load audio file. Note: use XMLHttpRequest to load the audio, should pay attention to cross-origin problem.
 70      */
 71     load: function(){
 72         if(!this._buffer){
 73             var request = new XMLHttpRequest();
 74             request.src = this.src;
 75             request.open('GET', this.src, true);
 76             request.responseType = 'arraybuffer';
 77             request.onload = this._onAudioEvent;
 78             request.onprogress = this._onAudioEvent;
 79             request.onerror = this._onAudioEvent;
 80             request.send();
 81             this._buffer = true;
 82         }
 83         return this;
 84     },
 85 
 86     /**
 87      * @language=en
 88      * @private
 89      */
 90     _onAudioEvent: function(e){
 91         // console.log('onAudioEvent:', e.type);
 92         var type = e.type;
 93 
 94         switch(type){
 95             case 'load':
 96                 var request = e.target;
 97                 request.onload = request.onprogress = request.onerror = null;
 98                 this._context.decodeAudioData(request.response, this._onDecodeComplete, this._onDecodeError);
 99                 request = null;
100                 break;
101             case 'ended':
102                 this.playing = false;
103                 this.fire('end');
104                 if(this.loop) this._doPlay();
105                 break;
106             case 'progress':
107                 this.fire(e);
108                 break;
109             case 'error':
110                 this.fire(e);
111                 break;
112         }
113     },
114 
115     /**
116      * @language=en
117      * @private
118      */
119     _onDecodeComplete: function(audioBuffer){
120         this._buffer = audioBuffer;
121         this.loaded = true;
122         this.duration = audioBuffer.duration;
123         // console.log('onDecodeComplete:', audioBuffer.duration);
124         this.fire('load');
125         if(this.autoPlay) this._doPlay();
126     },
127 
128     /**
129      * @language=en
130      * @private
131      */
132     _onDecodeError: function(){
133         this.fire('error');
134     },
135 
136     /**
137      * @language=en
138      * @private
139      */
140     _doPlay: function(){
141         this._clearAudioNode();
142 
143         var audioNode = this._context.createBufferSource();
144 
145         //some old browser are noteOn/noteOff -> start/stop
146         if(!audioNode.start){
147             audioNode.start = audioNode.noteOn;
148             audioNode.stop = audioNode.noteOff;
149         }
150 
151         audioNode.buffer = this._buffer;
152         audioNode.onended = this._onAudioEvent;
153         this._gainNode.gain.value = this.muted ? 0 : this.volume;
154         audioNode.connect(this._gainNode);
155         audioNode.start(0, this._offset);
156 
157         this._audioNode = audioNode;
158         this._startTime = this._context.currentTime;
159         this.playing = true;
160     },
161 
162     /**
163      * @language=en
164      * @private
165      */
166     _clearAudioNode: function(){
167         var audioNode = this._audioNode;
168         if(audioNode){
169             audioNode.onended = null;
170             // audioNode.disconnect(this._gainNode);
171             audioNode.disconnect(0);
172             this._audioNode = null;
173         }
174     },
175 
176     /**
177      * @language=en
178      * Play the audio. Restart playing the audio from the beginning if already playing.
179      */
180     play: function(){
181         if(this.playing) this.stop();
182 
183         if(this.loaded){
184             this._doPlay();
185         }else if(!this._buffer){
186             this.autoPlay = true;
187             this.load();
188         }
189 
190         return this;
191     },
192 
193     /**
194      * @language=en
195      * Pause (halt) playing the audio.
196      */
197     pause: function(){
198         if(this.playing){
199             this._audioNode.stop(0);
200             this._offset += this._context.currentTime - this._startTime;
201             this.playing = false;
202         }
203         return this;
204     },
205 
206     /**
207      * @language=en
208      * Continue to play the audio.
209      */
210     resume: function(){
211         if(!this.playing){
212             this._doPlay();
213         }
214         return this;
215     },
216 
217     /**
218      * @language=en
219      * Stop playing the audio.
220      */
221     stop: function(){
222         if(this.playing){
223             this._audioNode.stop(0);
224             this._audioNode.disconnect();
225             this._offset = 0;
226             this.playing = false;
227         }
228         return this;
229     },
230 
231     /**
232      * @language=en
233      * Set the volume.
234      */
235     setVolume: function(volume){
236         if(this.volume != volume){
237             this.volume = volume;
238             this._gainNode.gain.value = volume;
239         }
240         return this;
241     },
242 
243     /**
244      * @language=en
245      * Set mute mode.
246      */
247     setMute: function(muted){
248         if(this.muted != muted){
249             this.muted = muted;
250             this._gainNode.gain.value = muted ? 0 : this.volume;
251         }
252         return this;
253     },
254 
255     Statics: /** @lends WebAudio */ {
256         /**
257          * @language=en
258          * Does the browser support WebAudio.
259          */
260         isSupported: AudioContext != null,
261 
262         /**
263          * @language=en
264          * Does browser activate WebAudio already.
265          */
266         enabled: false,
267 
268         /**
269          * @language=en
270          * Activate WebAudio. Note: Require user action events to activate. Once activated, can play audio without user action events.
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 });
285 
286 })();