OSDN Git Service

Version 0.6.182, fix X.UA.
[pettanr/clientJs.git] / 0.6.x / js / 07_audio / 01_XWebAudio.js
index 8299229..966f667 100644 (file)
+var X_Audio_constructor = 3.1 <= X_UA[ 'Safari' ] && X_UA[ 'Safari' ] < 4 ?
+                                                               function( s, a ){
+                                                                       a = document.createElement( 'audio' );
+                                                                       a.src = s;
+                                                                       a.load();
+                                                                       return a;
+                                                               } :
+                                               // Android1.6 + MobileOpera12 HTMLAudio はいるが呼ぶとクラッシュする
+                                                 !( X_UA[ 'Android' ] < 2 ) ?
+                                                               window[ 'Audio' ] || window.HTMLAudioElement : null,
+       
+       // Blink5 Opera32 Win8 は HTMLAudio が壊れている、WebAudio は mp3 がデコードに失敗、ogg が動作
+       X_Audio_blinkOperaFix = X_UA[ 'BlinkOpera' ] && X_UA[ 'Windows' ],
 
-var X_Audio_WebAudio_context   = window.webkitAudioContext || window.AudioContext,
-       X_Audio_WebAudio_LIVE_LIST = [],
-       X_Audio_WebAudio_POOL_LIST = [],
-       X_Audio_WebAudio, X_Audio_WebAudioWrapper, X_Audio_rawAudio;
+       X_Audio_codecs;
 
-if( X_Audio_WebAudio_context ){
-       
-       X_Audio_WebAudio_context = new X_Audio_WebAudio_context;
+if( X_Audio_constructor ){
+       //http://himaxoff.blog111.fc2.com/blog-entry-97.html
+       //引数なしで new Audio() とすると、Operaでエラーになるそうなので注意。
+       X_TEMP.rawAudio = new X_Audio_constructor( '' );
        
-       function getWebAudioWrapper( proxy ){
-               var i = X_Audio_WebAudio_LIVE_LIST.length;
-               for( ; i; ){
-                       if( X_Audio_WebAudio_LIVE_LIST[ --i ].proxy === proxy ) return X_Audio_WebAudio_LIVE_LIST[ i ];
+       // https://html5experts.jp/miyuki-baba/3766/
+       // TODO Chrome for Android31 で HE-AAC が低速再生されるバグ
+       // TODO Android4 標準ブラウザで ogg のシークが正しくない!
+       if( X_TEMP.rawAudio.canPlayType ){
+               X_Audio_codecs = {
+                 'mp3'  : X_TEMP.rawAudio.canPlayType('audio/mpeg'),
+                 'opus' : X_TEMP.rawAudio.canPlayType('audio/ogg; codecs="opus"'),
+                 'ogg'  : X_TEMP.rawAudio.canPlayType('audio/ogg; codecs="vorbis"'),
+                 'wav'  : X_TEMP.rawAudio.canPlayType('audio/wav; codecs="1"'),
+                 'aac'  : X_TEMP.rawAudio.canPlayType('audio/aac'),
+                 'm4a'  : X_TEMP.rawAudio.canPlayType('audio/x-m4a') + X_TEMP.rawAudio.canPlayType('audio/m4a') + X_TEMP.rawAudio.canPlayType('audio/aac'),
+                 'mp4'  : X_TEMP.rawAudio.canPlayType('audio/x-mp4') + X_TEMP.rawAudio.canPlayType('audio/mp4') + X_TEMP.rawAudio.canPlayType('audio/aac'),
+                 'weba' : X_TEMP.rawAudio.canPlayType('audio/webm; codecs="vorbis"')
+               };
+               (function( X_Audio_codecs, k, v ){
+                       for( k in X_Audio_codecs ){
+                               //if( X_EMPTY_OBJECT[ k ] ) continue;
+                               v = X_Audio_codecs[ k ];
+                               v = v && !!( v.split( 'no' ).join( '' ) );
+                               if( v ){
+                                       console.log( k + ' ' + X_Audio_codecs[ k ] );
+                                       X_Audio_codecs[ k ] = true;
+                               } else {
+                                       delete X_Audio_codecs[ k ];
+                               };
+                       };
+                       if( X_Audio_blinkOperaFix ) delete X_Audio_codecs[ 'mp3' ];
+               })( X_Audio_codecs );
+       } else {
+               // iOS3.2.3
+               X_Audio_codecs = {
+                 'mp3'  : X_UA[ 'IE' ] || X_UA[ 'Chrome' ] || ( X_UA[ 'Windows' ] && X_UA[ 'Safari' ]  ),
+                 'ogg'  : 5 <= X_UA[ 'Gecko' ] || X_UA[ 'Chrome' ] || X_UA[ 'Opera' ] ,
+                 'wav'  : X_UA[ 'Gecko' ] || X_UA[ 'Opera' ] || ( X_UA[ 'Windows' ] && X_UA[ 'Safari' ]  ),
+                 'aac'  : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
+                 'm4a'  : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
+                 'mp4'  : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
+                 'weba' : 2 <= X_UA[ 'Gecko' ] || 10.6 <= X_UA[ 'Opera' ] // firefox4+(Gecko2+)
                };
+               (function( X_Audio_codecs, k ){
+                       for( k in X_Audio_codecs ){
+                               //if( X_EMPTY_OBJECT[ k ] ) continue;
+                               if( X_Audio_codecs[ k ] ){
+                                       console.log( k + ' ' + X_Audio_codecs[ k ] );
+                                       X_Audio_codecs[ k ] = true;
+                               } else {
+                                       delete X_Audio_codecs[ k ];
+                               };
+                       };
+               })( X_Audio_codecs );
        };
        
-       X_Audio_WebAudio = 
-               {
-                       backendName : 'Web Audio',
+       if( X_Audio_blinkOperaFix ){
+               X_Audio_constructor = null;
+               delete X_TEMP.rawAudio;
+       };
+};
 
-                       detect : function( proxy, source, ext ){
-                               var ok = ext === 'mp3' || ext === 'ogg';
-                               
-                               proxy.asyncDispatch( ok ? 'support' : 'nosupport' );
-                       },
-                       
-                       register : function( proxy, source, option ){
-                               X_Audio_WebAudio_LIVE_LIST.push( new X_Audio_WebAudioWrapper( proxy, source, option ) );
-                       },
-                       
-                       close : function( proxy ){
-                               return getWebAudioWrapper( proxy ).close();
-                       },
-                       
-                       play : function( proxy, startTime, endTime, loop, loopStartTime ){
-                               return getWebAudioWrapper( proxy ).play( startTime, endTime, loop, loopStartTime );
-                       },
-                       
-                       pause : function( proxy ){
-                               return getWebAudioWrapper( proxy ).pause();
-                       },
-       
-                       state : function( proxy, obj ){
-                               return getWebAudioWrapper( proxy ).state( obj );
-                       }
-               };
+
+var X_WebAudio_context      =  // 4s 以下ではない iPad 2G または iPad mini 1G 以下ではない, iPod touch 4G 以下ではない
+                                                               !X_UA[ 'iPhone_4s' ]  && !X_UA[ 'iPad_2Mini1' ]  && !X_UA[ 'iPod_4' ]  &&
+                                                               // Android2 + Gecko で WebAudio が極めて不安定
+                                                               !( X_UA[ 'Fennec' ] && X_UA[ 'Android' ] < 3 ) &&
+                                                               // Firefox40.0.5 + Windows8 で音声が途中から鳴らなくなる
+                                                               // Firefox41.0.1 + Windows8 で音声が途中から鳴らなくなる
+                                                               !( 40 <= X_UA[ 'Gecko' ] && X_UA[ 'Gecko' ] < 42 && X_UA[ 'Windows' ] ) &&
+                                                               ( window[ 'AudioContext' ] || window[ 'webkitAudioContext' ] ),
+       X_WebAudio_BUFFER_LIST  = [],
+       X_WebAudio_need1stTouch = X_UA[ 'iOS' ],
+       X_WebAudio,
+       X_WebAudio_BufferLoader,
+       X_WebAudio_fpsFix;
+
+/*
+ * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
+ */
+if( X_WebAudio_context ){
        
-       X_Audio_BACKENDS.push( X_Audio_WebAudio );
+       X_WebAudio_context = new X_WebAudio_context;
        
-       X_Audio_WebAudioWrapper = X.EventDispatcher.inherits(
-               'X.AV.WebAudioWrapper',
-               X.Class.POOL_OBJECT,
+       X_WebAudio_BufferLoader = X_EventDispatcher[ 'inherits' ](
+               'X.WebAudio.BufferLoader',
+               X_Class.POOL_OBJECT,
                {
-                       
-                       proxy           : null,
-                       
-                       startTime       : 0,
-                       endTime         : 0,
-                       loopStartTime   : 0,
-                       seekTime        : 0,
-                       duration        : 0,
-                       
-                       playing         : false,
-                       error           : 0,                    
-                       loop            : false,
-                       volume          : 0.5,
-                                               
-                       _startTime      : 0,
-                       _playTime       : 0,
-            _timerID        : 0,
-            _interval       : 0,
-               buffer          : null,
-            gainNode        : null,
-            _onended        : null,
-            
+                       audioUrl        : '',
             xhr             : null,
             onDecodeSuccess : null,
             onDecodeError   : null,
             
-                       Constructor : function( proxy, source, option ){
-                               this.closed = false;
-                               this.proxy  = proxy;
-                               this.xhr    = X.Net.xhrGet( source, 'arraybuffer' )
-                                                               .listen( X.Event.PROGRESS, this )
-                                                               .listenOnce( [ X.Event.SUCCESS, X.Event.COMPLETE, X.Event.CANCELED ], this );
-                               X_AudioWrapper_updateStates( this, option );
+            audioBuffer     : null,
+            errorState      : 0,
+            webAudioList    : null,
+            
+                       'Constructor' : function( webAudio, url ){
+                               this.webAudioList = [ webAudio ];
+                               this.audioUrl     = url;
+                               this.xhr = X[ 'Net' ]( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
+                                                                       [ 'listen' ]( X_EVENT_PROGRESS, this )
+                                                                       [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
+                               X_WebAudio_BUFFER_LIST.push( this );
                        },
                        
                        handleEvent : function( e ){
                                switch( e.type ){
-                                       case X.Event.PROGRESS :
-                                               e.percent ?
-                                                       this.proxy.dispatch( { type : 'progress', percent : e.percent } ) :
-                                                       this.proxy.dispatch( 'loadstart' );
+                                       case X_EVENT_PROGRESS :
+                                               this[ 'dispatch' ]( { type : 'progress', 'percent' : e[ 'percent' ] } );
                                                return;
                                        
-                                       case X.Event.SUCCESS :
-                                               X_Audio_WebAudio_context.decodeAudioData( e.data,
-                                                       this.callbackDecodeSuccess = X_Callback_create( this, this._onDecodeSuccess ),
-                                                       this.callbackDecodeError   = X_Callback_create( this, this._onDecodeError ) );
-                                               break;
-
-                                       case X.Event.CANCELED :
-                                               this.error = 1;
-                                               this.proxy.dispatch( 'aborted' );
+                                       case X_EVENT_SUCCESS :
+                                       // TODO 旧api
+                                       // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
+                                       
+                                       // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
+                                       // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
+                                       // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
+                                       // ただし iOS 8.1.2 では エラーになる
+                                               if( X_UA[ 'iOS' ] < 8 || !X_WebAudio_context[ 'decodeAudioData' ] ){
+                                                       this._onDecodeSuccess( X_WebAudio_context[ 'createBuffer' ]( e.response, false ) );
+                                               } else
+                                               if( X_WebAudio_context[ 'decodeAudioData' ] ){
+                                                       X_WebAudio_context[ 'decodeAudioData' ]( e.response,
+                                                               this.onDecodeSuccess = X_Closure_create( this, this._onDecodeSuccess ),
+                                                               this.onDecodeError   = X_Closure_create( this, this._onDecodeError ) );
+                                               };
                                                break;
 
-                                       case X.Event.COMPLETE :
-                                               this.error = 2;                         
-                                               this.proxy.asyncDispatch( { type : 'error', message : 'xhr error' } );
+                                       case X_EVENT_COMPLETE :
+                                               this.errorState = 1;                            
+                                               this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
                                                break;
                                };
-                               this.xhr.unlisten( [ X.Event.PROGRESS, X.Event.SUCCESS, X.Event.COMPLETE, X.Event.CANCELED ], this );
+                               this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
                                delete this.xhr;
                        },
                        
                                _onDecodeSuccess : function( buffer ){
-                                       this._onDecodeComplete();
+                                       this.onDecodeSuccess && this._onDecodeComplete();
                                        
                        if ( !buffer ) {
-                           this.proxy.asyncDispatch( { type : 'error', message : 'buffer is ' + buffer } );
+                               this.errorState = 2;
+                           this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
                            return;
                        };
-       
-                       this.buffer   = buffer;
-                       this.duration = buffer.duration * 1000;
-                       this.endTime  = this.endTime || this.duration;
                        
-                       this.proxy.asyncDispatch( 'loadedmetadata' );
-                       this.proxy.asyncDispatch( 'loadeddata' );
-                       this.proxy.asyncDispatch( 'canplay' );
-                       this.proxy.asyncDispatch( 'canplaythrough' );
+                       console.log( 'WebAudio decode success!' );
+       
+                       this.audioBuffer = buffer;
+
+                                       this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
+
+                       console.log( 'WebAudio decoded!' );
                                },
                                
                                _onDecodeError : function(){
+                                       console.log( 'WebAudio decode error!' );
                                        this._onDecodeComplete();
-                                       this.error = 3;
-                                       this.proxy.asyncDispatch( { type : 'error', message : 'decode error' } );
+                                       this.errorState = 2;
+                                       this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
                                },
                                
                                _onDecodeComplete : function(){
-                                       X_Callback_correct( this.callbackDecodeSuccess );
-                                       delete this.callbackDecodeSuccess;
-                                       X_Callback_correct( this.callbackDecodeError );
-                                       delete this.callbackDecodeError;
+                                       X_Closure_correct( this.onDecodeSuccess );
+                                       delete this.onDecodeSuccess;
+                                       X_Closure_correct( this.onDecodeError );
+                                       delete this.onDecodeError;
                                },
                        
-                       close : function(){     
-                   delete this.buffer;
-       
-                               this.playing  && this.pause();
-                   this.source   && this._sourceDispose();
+                       unregister : function( webAudio ){
+                               var list = this.webAudioList,
+                                       i    = list.indexOf( webAudio );
+                               if( 0 < i ){
+                                       list.splice( i, 1 );
+                                       if( list.length ){
+                                               this.xhr && this.xhr[ 'kill' ]();
+                                               this[ 'kill' ]();
+                                       };
+                               };
+                       }
+                       
+               }
+       );
        
-                   this._onended && X_Callback_correct( this._onended );       
        
-                   this.gainNode && this.gainNode.disconnect();
+       X_WebAudio = X_AudioBase[ 'inherits' ](
+               'X.WebAudio',
+               X_Class.POOL_OBJECT,
+               {
+                       
+                       loader          : null,
+                                               
+                       _startPos       : 0,
+                       _endPosition    : 0,
+                       _startTime      : 0,
+            _timerID        : 0,
+            _interval       : 0,
+               audioBuffer     : null,
+               bufferSource    : null,
+            gainNode        : null,
+            _onended        : null,
+            
+                       'Constructor' : function( disatcher, url, option ){                             
+                               var i = 0,
+                                       l = X_WebAudio_BUFFER_LIST.length,
+                                       loader;
+
+                               /*
+                                * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
+                                * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
+                                * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
+                                */
+                               if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] && !X_WebAudio_fpsFix ){
+                                       X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } );
+                                       X_WebAudio_fpsFix = true;
+                               };
+
+                               for( ; i < l; ++i ){
+                                       loader = X_WebAudio_BUFFER_LIST[ i ];
+                                       if( loader.audioUrl === url ){
+                                               this.loader = loader;
+                                               loader.webAudioList.push( this );
+                                               break;
+                                       };
+                               };
+                               
+                               if( !this.loader ){
+                                       this.loader = loader = X_WebAudio_BufferLoader( this, url );
+                               };
+                               
+                               this.disatcher = disatcher || this;
+                               this.setState( option );
+                               
+                               this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
+                               
+                               if( loader.audioBuffer || loader.errorState ){
+                                       this._onLoadBufferComplete();
+                               } else {
+                                       loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
+                               };
                        },
                        
-                               _sourceDispose : function(){
-                           this.source && this.source.disconnect();
-                           delete this.source.onended;
-                           delete this.source;
-                       },
+                       onKill : function(){
+                               this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
+                                       .unregister( this );
+
+                               delete this.audioBuffer;
+                               
+                               this.playing      && this.actualPause();
+                   this.bufferSource && this._sourceDispose();
+       
+                   this._onended     && X_Closure_correct( this._onended );    
+       
+                   this.gainNode     && this.gainNode.disconnect();
+                       },
+                               _onLoadBufferComplete : function( e ){
+                                       var loader = this.loader,
+                                               buffer = loader.audioBuffer;
+                                       
+                                       e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
+                                       
+                       if ( !buffer ) {
+                               this.error = loader.errorState;
+                               
+                           this.disatcher[ 'dispatch' ]({
+                                                               type    : X_EVENT_ERROR,
+                                                               error   : loader.errorState,
+                                                               message : loader.errorState === 1 ?
+                                                                                       'load buffer network error' :
+                                                                                       'buffer decode error'
+                                                       });
+                                               this[ 'kill' ]();
+                           return;
+                       };
+       
+                       this.audioBuffer = buffer;
+                       this.duration    = buffer.duration * 1000;
+
+                                       this.disatcher[ 'asyncDispatch' ]( X_EVENT_READY );
+                       
+                       console.log( 'WebAudio buffer ready' );
+                       
+                       this.autoplay && X_Timer_once( 16, this, this.play );
+                                       
+                               },
                        
-                       play : function( seekTime ){
-                               var begin;
+                       actualPlay : function(){
+                               var begin, end;
                                
-                   if( !this.buffer ) return this;
+                               console.log( '[WebAudio] play abuf:' + !!this.audioBuffe );
                                
-                               begin = ( seekTime || seekTime === 0 ) ? seekTime : this.playing ? this.loopStartTime : this.startTime;
+                   if( !this.audioBuffer ){
+                       this.autoplay = true;
+                       return;
+                   };
                                
-                               console.log( '[WebAudio] play' );
+                               end   = X_Audio_getEndTime( this );
+                               begin = X_Audio_getStartTime( this, end, true );
                                
-                               if( this.source ) this._sourceDispose();
-                               if( !this.gainNode ) this.gainNode = X_Audio_WebAudio_context.createGain();
+                               console.log( '[WebAudio] play ' + begin + ' -> ' + end );
                                
-                   this.source        = X_Audio_WebAudio_context.createBufferSource();
-                   this.source.buffer = this.buffer;
-                   this.source.connect( this.gainNode );
-                   
-                   this.gainNode.connect( X_Audio_WebAudio_context.destination );
-                   this.gainNode.gain.value = this.volume;
+                               if( this.bufferSource ) this._sourceDispose();
+                               if( !this.gainNode ){
+                                       this.gainNode = X_WebAudio_context[ 'createGain' ] ? X_WebAudio_context[ 'createGain' ]() : X_WebAudio_context[ 'createGainNode' ]();
+                       this.gainNode[ 'connect' ]( X_WebAudio_context[ 'destination' ] );
+                               };
+                   this.bufferSource        = X_WebAudio_context[ 'createBufferSource' ]();
+                   this.bufferSource.buffer = this.audioBuffer;
+                   this.bufferSource[ 'connect' ]( this.gainNode );
                    
-                   this._timerID && X.Timer.remove( this._timerID );
+                   this.gainNode[ 'gain' ].value = this.gain;
                    
-                   // おかしい、stop 前に外していても呼ばれる、、、@Firefox
-                if( this.source.onended !== undefined ){
+                   // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
+                   // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
+                   // 多くのブラウザで onended は timer を使ったカウントより遅いので使わない
+                //if( this.bufferSource.onended !== undefined ){
                        //console.log( '> use onended' );
-                       this.source.onended = this._onended || ( this._onended = X_Callback_create( this, this._onEnded ) );
-                } else {
-                                       this._timerID = X.Timer.once( this.endTime - begin, this, this._onEnded );
-                };
+                       //this.bufferSource.onended = this._onended || ( this._onended = X_Closure_create( this, this._onEnded ) );
+                //} else {
+                       this._timerID && X_Timer_remove( this._timerID );
+                                       this._timerID = X_Timer_once( end - begin, this, this._onEnded );
+                //};
        
-                   if( this.source.start ){
-                       this.source.start( 0, begin / 1000, this.endTime / 1000 );
+                   if( this.bufferSource.start ){
+                       this.bufferSource.start( 0, begin / 1000, end / 1000 );
                    } else {
-                       this.source.noteGrainOn( 0, begin / 1000, this.endTime / 1000 );
+                       this.bufferSource[ 'noteGrainOn' ]( 0, begin / 1000, end / 1000 );
                    };
                    
-                   this.playing    = true;
-                   this._startTime = begin;
-                   this._playTime  = X_Audio_WebAudio_context.currentTime * 1000;
-                   this._interval  = this._interval || X.Timer.add( 1000, 0, this, this._onInterval );
+                   this.playing      = true;
+                   this._startPos    = begin;
+                   this._endPosition = end;
+                   this._startTime   = X_WebAudio_context.currentTime * 1000;
+                   this._interval    = this._interval || X_Timer_add( 1000, 0, this, this._onInterval );
                        },
+                       
+                               _sourceDispose : function(){
+                           this.bufferSource.disconnect();
+                           delete this.bufferSource.onended;
+                           delete this.bufferSource;
+                       },
 
                                _onInterval : function(){
                                        if( !this.playing ){
                                                delete this._interval;
-                                               return X_Callback_UN_LISTEN;
+                                               return X_CALLBACK_UN_LISTEN;
                                        };
-                                       this.proxy.dispatch( 'timeupdate' );
+                                       this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
                                },
                                                
                                _onEnded : function(){
+                                       var time;
                                        delete this._timerID;
+                                       
                            if( this.playing ){
-                               console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime ) ) + ' < ' + ( this.endTime - this._startTime ) );
-                               // Firefox 用の対策,,,
-                               if( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime < this.endTime - this._startTime ) return;
+                               time = X_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
+                               //console.log( '> onEnd ' + ( this.playing && ( X_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
+                               if( this._onended ){
+                                       // Firefox 用の対策,,,
+                                       if( time < 0 ) return;
+                               } else {
+                                       if( time < 0 ){
+                                               //console.log( '> onEnd crt:' + ( X_WebAudio_context.currentTime * 1000 ) + ' startTime:' + this._startTime +
+                                               //      ' from:' + this._startPos + ' to:' + this._endPosition );
+                                               this._timerID = X_Timer_once( -time, this, this._onEnded );
+                                               return;
+                                       };
+                               };
                                
-                               if( this.loop ){
-                                       this.play();
+                               if( this.autoLoop ){
+                                       if( !( this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
+                                               this.looped = true;
+                                               this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
+                                               this.actualPlay();
+                                       };
                                } else {
-                                       this.pause();
-                                       this.proxy.dispatch( 'ended' );
+                                       this.actualPause();
+                                       this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
                                };
                            };
                                },
                        
-                       pause : function(){
-                               if( !this.playing ) return this;
+                       actualPause : function(){
+                               //if( !this.playing ) return this;
                                
                                console.log( '[WebAudio] pause' );
                                
-                   this._timerID && X.Timer.remove( this._timerID );
+                               this.seekTime = this.getActualCurrentTime();
+                               
+                   this._timerID && X_Timer_remove( this._timerID );
                                delete this._timerID;
                                delete this.playing;
 
-                   if( this.source ){
-                       if( this.source.onended ) delete this.source.onended;
+                   if( this.bufferSource ){
+                       if( this.bufferSource.onended ) delete this.bufferSource.onended;
                        
-                       this.source.stop ? 
-                               this.source.stop( 0 ) : this.source.noteOff( 0 );
+                       this.bufferSource.stop ? 
+                               this.bufferSource.stop( 0 ) : this.bufferSource[ 'noteOff' ]( 0 );
                    };
                        },
-       
-                       state : function( obj ){
-                               var time = this.playing ? ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime ) : 0,
-                                       result, halfway;
-                               
-                               if( obj === undefined ){
-                                   return {
-                                       startTime     : this.startTime,
-                                       endTime       : this.endTime,
-                                       loopStartTime : this.loopStartTime,
-                                       currentTime   : time + this._startTime,
-                                       loop          : this.loop,
-                                       volume        : this.volume,
-                                       error         : this.error,
-                                       playing       : this.playing,                           
-                                       duration      : this.duration
-                                   };                                  
-                               };
                        
-                               result = X_AudioWrapper_updateStates( this, obj );
-                               
-                               if( result & 2 ){ // seek
-                       this.play( this.seekTime );
-                       delete this.seekTime;
-                               } else
-                               if( result & 1 ){
-                                       this.play();
+                       getActualCurrentTime : function(){
+                               return X_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
+                       },
+                       
+                       afterUpdateState : function( result ){
+                               if( result & 2 || result & 1 ){ // seek
+                       this.actualPlay();
                                } else
                                if( result & 4 ){
-                      this.gainNode.gain.value = this.volume;
+                      this.gainNode[ 'gain' ].value = this.gain;
                                };
                        }
 
                }
        );
 
+       X_Audio_BACKENDS.push(
+               {
+                       backendID   : 1,
+                       
+                       backendName : 'WebAudio',
+
+                       canPlay     : X_Audio_codecs,
+
+                       detect      : function( proxy, source, ext ){
+                               proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
+                       },
+                       
+                       klass : X_WebAudio
+               }
+       );
 };