      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 })();