1 /** 2 * Hilo 3 * Copyright 2015 alibaba.com 4 * Licensed under the MIT License 5 */ 6 7 /** 8 * @class WebAudio audio playing module. It provides a better way to play and control audio, use on iOS6+ platform. 9 * Compatibility:iOS6+、Chrome33+、Firefox28+ supported,but all Android browsers do not support. 10 * @param {Object} properties create object properties, include all writable properties of this class. 11 * @module hilo/media/WebAudio 12 * @requires hilo/core/Class 13 * @requires hilo/util/util 14 * @requires hilo/event/EventMixin 15 * @property {String} src The source of the playing audio. 16 * @property {Boolean} loop Is loop playback, default value is false. 17 * @property {Boolean} autoPlay Is the audio autoplay, default value is false. 18 * @property {Boolean} loaded Is the audio resource loaded, readonly! 19 * @property {Boolean} playing Is the audio playing, readonly! 20 * @property {Number} duration The duration of the audio, readonly! 21 * @property {Number} volume The volume of the audio, value between 0 to 1. 22 * @property {Boolean} muted Is the audio muted, default value is 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 Initialize. 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 * Load audio file. Note: use XMLHttpRequest to load the audio, should pay attention to cross-origin problem. 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 * Play the audio. Restart playing the audio from the beginning if already playing. 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 * Pause (halt) playing the audio. 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 * Continue to play the audio. 215 */ 216 resume: function(){ 217 if(!this.playing){ 218 this._doPlay(); 219 } 220 return this; 221 }, 222 223 /** 224 * Stop playing the audio. 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 * Set the volume. 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 * Set mute mode. 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 * Does the browser support WebAudio. 261 */ 262 isSupported: context !== null, 263 264 /** 265 * Does browser activate WebAudio already. 266 */ 267 enabled: false, 268 269 /** 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 * The audio buffer caches. 285 * @private 286 * @type {Object} 287 */ 288 _bufferCache:{}, 289 /** 290 * Clear the audio buffer cache. 291 * @param {String} url audio's url. if url is none, it will clear all buffer. 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 })();