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 // ただし iOS 8.1.2 では エラーになる
95 if( X_Audio_WebAudio_context.createBuffer && X_UA[ 'iOS' ] < 8 ){
96 this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) );
98 if( X_Audio_WebAudio_context.decodeAudioData ){
99 X_Audio_WebAudio_context.decodeAudioData( e.data,
100 this.onDecodeSuccess = X_Callback_create( this, this._onDecodeSuccess ),
101 this.onDecodeError = X_Callback_create( this, this._onDecodeError ) );
103 this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) );
107 case X_EVENT_CANCELED :
109 this.proxy[ 'dispatch' ]( 'aborted' );
112 case X_EVENT_COMPLETE :
114 this.proxy[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'xhr error' } );
117 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE, X_EVENT_CANCELED ], this );
121 _onDecodeSuccess : function( buffer ){
122 console.log( 'WebAudio decode success!' );
124 this.onDecodeSuccess && this._onDecodeComplete();
127 this.proxy[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'buffer is ' + buffer } );
131 this.buffer = buffer;
132 this.duration = buffer.duration * 1000;
134 this.proxy[ 'asyncDispatch' ]( 'loadedmetadata' );
135 this.proxy[ 'asyncDispatch' ]( 'loadeddata' );
136 this.proxy[ 'asyncDispatch' ]( 'canplay' );
137 this.proxy[ 'asyncDispatch' ]( 'canplaythrough' );
139 this.proxy[ 'asyncDispatch' ]( X_EVENT_READY );
141 this.autoplay && X_Timer_once( 16, this, this.play );
143 console.log( 'WebAudio decoded!' );
146 _onDecodeError : function(){
147 console.log( 'WebAudio decode error!' );
148 this._onDecodeComplete();
150 this.proxy[ 'asyncDispatch' ]( { type : X_EVENT_ERROR, message : 'decode error' } );
153 _onDecodeComplete : function(){
154 X_Callback_correct( this.onDecodeSuccess );
155 delete this.onDecodeSuccess;
156 X_Callback_correct( this.onDecodeError );
157 delete this.onDecodeError;
160 _onBufferReady : function( e ){
161 var audio = X_Audio_WebAudio_getBuffer( this.url );
162 this._onDecodeSuccess( audio.buffer );
168 if( this.xhr ) this.xhr.close();
170 if( this.onDecodeSuccess ){
174 this.playing && this.pause();
175 this.source && this._sourceDispose();
177 this._onended && X_Callback_correct( this._onended );
179 this.gainNode && this.gainNode.disconnect();
182 _sourceDispose : function(){
183 this.source.disconnect();
184 delete this.source.onended;
192 this.autoplay = true;
196 end = X_AudioWrapper_getEndTime( this );
197 begin = X_AudioWrapper_getStartTime( this, end, true );
199 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
201 if( this.source ) this._sourceDispose();
202 if( !this.gainNode ){
203 this.gainNode = X_Audio_WebAudio_context.createGain ? X_Audio_WebAudio_context.createGain() : X_Audio_WebAudio_context.createGainNode();
204 this.gainNode.connect( X_Audio_WebAudio_context.destination );
206 this.source = X_Audio_WebAudio_context.createBufferSource();
207 this.source.buffer = this.buffer;
208 this.source.connect( this.gainNode );
210 this.gainNode.gain.value = this.volume;
212 // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
213 // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
214 if( false && this.source.onended !== undefined ){
215 //console.log( '> use onended' );
216 this.source.onended = this._onended || ( this._onended = X_Callback_create( this, this._onEnded ) );
218 this._timerID && X_Timer_remove( this._timerID );
219 this._timerID = X_Timer_once( end - begin, this, this._onEnded );
222 if( this.source.start ){
223 this.source.start( 0, begin / 1000, end / 1000 );
225 this.source.noteGrainOn( 0, begin / 1000, end / 1000 );
229 this._startPos = begin;
230 this._endPosition = end;
231 this._startTime = X_Audio_WebAudio_context.currentTime * 1000;
232 this._interval = this._interval || X_Timer_add( 1000, 0, this, this._onInterval );
235 _onInterval : function(){
237 delete this._interval;
238 return X_Callback_UN_LISTEN;
240 this.proxy[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
243 _onEnded : function(){
245 delete this._timerID;
248 time = X_Audio_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
249 //console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
252 if( time < 0 ) return;
255 console.log( '> onEnd crt:' + ( X_Audio_WebAudio_context.currentTime * 1000 ) + ' startTime:' + this._startTime +
256 ' from:' + this._startPos + ' to:' + this._endPosition );
257 this._timerID = X_Timer_once( -time, this, this._onEnded );
263 if( !( this.proxy[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_Callback_PREVENT_DEFAULT ) ){
265 this.proxy[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
270 this.proxy[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
276 if( !this.playing ) return this;
278 console.log( '[WebAudio] pause' );
280 this.seekTime = this.state().currentTime;
282 this._timerID && X_Timer_remove( this._timerID );
283 delete this._timerID;
287 if( this.source.onended ) delete this.source.onended;
290 this.source.stop( 0 ) : this.source.noteOff( 0 );
294 state : function( obj ){
297 if( obj === undefined ){
299 startTime : this.startTime,
300 endTime : this.endTime < 0 ? this.duration : this.endTime,
301 loopStartTime : this.loopStartTime < 0 ? this.startTime : this.loopStartTime,
302 loopEndTime : this.loopEndTime < 0 ? ( this.endTime || this.duration ) : this.loopEndTime,
304 looped : this.looped,
305 volume : this.volume,
306 playing : this.playing,
307 duration : this.duration,
309 currentTime : this.playing ? ( X_Audio_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0 ) : this.seekTime,
314 result = X_AudioWrapper_updateStates( this, obj );
316 if( result & 2 || result & 1 ){ // seek
320 this.gainNode.gain.value = this.volume;
328 X_Audio_BACKENDS.push(
330 backendName : 'Web Audio',
333 detect : function( proxy, source, ext ){
334 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
337 klass : X_Audio_WebAudioWrapper