2 var X_Audio_WebAudio_context = !X_UA.iPhone_4s && !X_UA.iPad_2Mini1 && !X_UA.iPod_4 &&
3 !( X_UA.Gecko && X_UA.Android ) &&
4 ( window.AudioContext || window.webkitAudioContext ),
5 X_Audio_WebAudioWrapper;
8 * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
10 if( X_Audio_WebAudio_context ){
12 X_Audio_WebAudio_context = new X_Audio_WebAudio_context;
14 function X_Audio_WebAudio_getBuffer( url ){
15 var i = 0, l = X_Audio_WRAPPER_LIST.length;
16 for( i = 0; i < l; ++i ){
17 if( X_Audio_WRAPPER_LIST[ i ].url === url ) return X_Audio_WRAPPER_LIST[ i ];
21 X_Audio_WebAudioWrapper = X.EventDispatcher.inherits(
22 'X.AV.WebAudioWrapper',
54 onDecodeSuccess : null,
57 Constructor : function( proxy, url, option ){
58 var audio = X_Audio_WebAudio_getBuffer( url );
63 X_AudioWrapper_updateStates( this, option );
65 if( audio && audio.buffer ){
66 this._onDecodeSuccess( audio.buffer );
69 // TODO 当てにしていたaudioがclose 等した場合
70 audio.proxy.listenOnce( 'canplaythrough', this, this._onBufferReady );
72 this.xhr = X.Net.xhrGet( url, 'arraybuffer' )
73 .listen( X.Event.PROGRESS, this )
74 .listenOnce( [ X.Event.SUCCESS, X.Event.COMPLETE, X.Event.CANCELED ], this );
78 handleEvent : function( e ){
80 case X.Event.PROGRESS :
82 this.proxy.dispatch( { type : 'progress', percent : e.percent } ) :
83 this.proxy.dispatch( 'loadstart' );
86 case X.Event.SUCCESS :
87 console.log( 'WebAudio xhr success! ' + !!X_Audio_WebAudio_context.decodeAudioData + ' t:' + typeof e.data );
89 // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
91 // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
92 // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
93 // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
94 if( X_Audio_WebAudio_context.createBuffer && X_UA.iOS ){
95 this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) );
97 if( X_Audio_WebAudio_context.decodeAudioData ){
98 X_Audio_WebAudio_context.decodeAudioData( e.data,
99 this.onDecodeSuccess = X_Callback_create( this, this._onDecodeSuccess ),
100 this.onDecodeError = X_Callback_create( this, this._onDecodeError ) );
102 this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) );
106 case X.Event.CANCELED :
108 this.proxy.dispatch( 'aborted' );
111 case X.Event.COMPLETE :
113 this.proxy.asyncDispatch( { type : X.Event.ERROR, message : 'xhr error' } );
116 this.xhr.unlisten( [ X.Event.PROGRESS, X.Event.SUCCESS, X.Event.COMPLETE, X.Event.CANCELED ], this );
120 _onDecodeSuccess : function( buffer ){
121 console.log( 'WebAudio decode success!' );
123 this.onDecodeSuccess && this._onDecodeComplete();
126 this.proxy.asyncDispatch( { type : X.Event.ERROR, message : 'buffer is ' + buffer } );
130 this.buffer = buffer;
131 this.duration = buffer.duration * 1000;
133 this.proxy.asyncDispatch( 'loadedmetadata' );
134 this.proxy.asyncDispatch( 'loadeddata' );
135 this.proxy.asyncDispatch( 'canplay' );
136 this.proxy.asyncDispatch( 'canplaythrough' );
138 this.proxy.asyncDispatch( X.Event.READY );
140 this.autoplay && X.Timer.once( 16, this, this.play );
142 console.log( 'WebAudio decoded!' );
145 _onDecodeError : function(){
146 console.log( 'WebAudio decode error!' );
147 this._onDecodeComplete();
149 this.proxy.asyncDispatch( { type : X.Event.ERROR, message : 'decode error' } );
152 _onDecodeComplete : function(){
153 X_Callback_correct( this.onDecodeSuccess );
154 delete this.onDecodeSuccess;
155 X_Callback_correct( this.onDecodeError );
156 delete this.onDecodeError;
159 _onBufferReady : function( e ){
160 var audio = X_Audio_WebAudio_getBuffer( this.url );
161 this._onDecodeSuccess( audio.buffer );
167 if( this.xhr ) this.xhr.close();
169 if( this.onDecodeSuccess ){
173 this.playing && this.pause();
174 this.source && this._sourceDispose();
176 this._onended && X_Callback_correct( this._onended );
178 this.gainNode && this.gainNode.disconnect();
181 _sourceDispose : function(){
182 this.source.disconnect();
183 delete this.source.onended;
191 this.autoplay = true;
195 end = X_AudioWrapper_getEndTime( this );
196 begin = X_AudioWrapper_getStartTime( this, end, true );
198 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
200 if( this.source ) this._sourceDispose();
201 if( !this.gainNode ){
202 this.gainNode = X_Audio_WebAudio_context.createGain ? X_Audio_WebAudio_context.createGain() : X_Audio_WebAudio_context.createGainNode();
203 this.gainNode.connect( X_Audio_WebAudio_context.destination );
205 this.source = X_Audio_WebAudio_context.createBufferSource();
206 this.source.buffer = this.buffer;
207 this.source.connect( this.gainNode );
209 this.gainNode.gain.value = this.volume;
211 // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
212 // 破棄された X.Callback が呼ばれて、obj._() でエラーになる。Firefox では、onended は使わない
213 if( false && this.source.onended !== undefined ){
214 //console.log( '> use onended' );
215 this.source.onended = this._onended || ( this._onended = X_Callback_create( this, this._onEnded ) );
217 this._timerID && X.Timer.remove( this._timerID );
218 this._timerID = X.Timer.once( end - begin, this, this._onEnded );
221 if( this.source.start ){
222 this.source.start( 0, begin / 1000, end / 1000 );
224 this.source.noteGrainOn( 0, begin / 1000, end / 1000 );
228 this._startTime = begin;
230 this._playTime = X_Audio_WebAudio_context.currentTime * 1000;
231 this._interval = this._interval || X.Timer.add( 1000, 0, this, this._onInterval );
234 _onInterval : function(){
236 delete this._interval;
237 return X_Callback_UN_LISTEN;
239 this.proxy.dispatch( X.Event.MEDIA_PLAYING );
242 _onEnded : function(){
244 delete this._timerID;
247 time = X_Audio_WebAudio_context.currentTime * 1000 - this._playTime - this._endTime + this._startTime | 0;
248 //console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime ) ) + ' < ' + ( this._endTime - this._startTime ) );
251 if( time < 0 ) return;
254 console.log( '> onEnd ' + ( -time ) + ' start:' + this._startTime + '-' + this._endTime );
255 this._timerID = X.Timer.once( -time, this, this._onEnded );
261 if( !( this.proxy.dispatch( X.Event.MEDIA_BEFORE_LOOP ) & X.Callback.PREVENT_DEFAULT ) ){
263 this.proxy.dispatch( X.Event.MEDIA_LOOPED );
268 this.proxy.dispatch( X.Event.MEDIA_ENDED );
274 if( !this.playing ) return this;
276 console.log( '[WebAudio] pause' );
278 this.seekTime = this.state().currentTime;
280 this._timerID && X.Timer.remove( this._timerID );
281 delete this._timerID;
285 if( this.source.onended ) delete this.source.onended;
288 this.source.stop( 0 ) : this.source.noteOff( 0 );
292 state : function( obj ){
295 if( obj === undefined ){
297 startTime : this.startTime,
298 endTime : this.endTime < 0 ? this.duration : this.endTime,
299 loopStartTime : this.loopStartTime < 0 ? this.startTime : this.loopStartTime,
300 loopEndTime : this.loopEndTime < 0 ? ( this.endTime || this.duration ) : this.loopEndTime,
302 looped : this.looped,
303 volume : this.volume,
304 playing : this.playing,
305 duration : this.duration,
307 currentTime : this.playing ? ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime + this._startTime | 0 ) : this.seekTime,
312 result = X_AudioWrapper_updateStates( this, obj );
314 if( result & 2 || result & 1 ){ // seek
318 this.gainNode.gain.value = this.volume;
326 X_Audio_BACKENDS.push(
328 backendName : 'Web Audio',
331 detect : function( proxy, source, ext ){
332 proxy.asyncDispatch( X_Audio_codecs[ ext ] ? X_Audio_CAN_PLAY : X_Audio_NOT_PLAY );
335 klass : X_Audio_WebAudioWrapper