X-Git-Url: http://git.osdn.jp/view?a=blobdiff_plain;f=0.6.x%2Fjs%2F07_audio%2F01_XWebAudio.js;h=00cf9c3edc27ab914580c8e73803d06ac01fc2aa;hb=0a4e04fb0af6e1b2e452d1a8c0822e723d32a0ee;hp=8939e403f09665c6eb1f507ec56bca3295797c43;hpb=4533342ee7fb302488e76a207a129bb18866c00f;p=pettanr%2FclientJs.git diff --git a/0.6.x/js/07_audio/01_XWebAudio.js b/0.6.x/js/07_audio/01_XWebAudio.js index 8939e40..00cf9c3 100644 --- a/0.6.x/js/07_audio/01_XWebAudio.js +++ b/0.6.x/js/07_audio/01_XWebAudio.js @@ -1,88 +1,55 @@ -var X_Audio_WebAudio_context = window.webkitAudioContext || window.AudioContext, +var X_Audio_WebAudio_context = !X_UA[ 'iPhone_4s' ] && !X_UA[ 'iPad_2Mini1' ] && !X_UA[ 'iPod_4' ] && + !( X_UA[ 'Gecko' ] && X_UA[ 'Android' ] ) && + ( window.AudioContext || window.webkitAudioContext ), + X_Audio_BUFFER_LIST = [], X_Audio_WebAudioWrapper; -if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_context ){ +/* + * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可 + */ +if( X_Audio_WebAudio_context ){ X_Audio_WebAudio_context = new X_Audio_WebAudio_context; - function X_Audio_WebAudio_getBuffer( url ){ - var i = 0, l = X_Audio_WRAPPER_LIST.length; - for( i = 0; i < l; ++i ){ - if( X_Audio_WRAPPER_LIST[ i ].url === url ) return X_Audio_WRAPPER_LIST[ i ]; - }; - }; - - X_Audio_WebAudioWrapper = X.EventDispatcher.inherits( - 'X.AV.WebAudioWrapper', - X.Class.POOL_OBJECT, + X_Audio_BufferLoader = X_EventDispatcher[ 'inherits' ]( + 'X.AV.WebAudioBufferLoader', + X_Class.POOL_OBJECT, { - url : '', - proxy : null, - - startTime : 0, - endTime : -1, - loopStartTime : -1, - loopEndTime : -1, - seekTime : -1, - duration : 0, - - playing : false, - error : 0, - loop : false, - looped : false, - autoplay : false, - volume : 0.5, - - _startTime : 0, - _endTime : 0, - _playTime : 0, - _timerID : 0, - _interval : 0, - buffer : null, - source : null, - gainNode : null, - _onended : null, - xhr : null, onDecodeSuccess : null, onDecodeError : null, - Constructor : function( proxy, url, option ){ - var audio = X_Audio_WebAudio_getBuffer( url ); - + buffer : null, + error : 0, + webAudioList : null, + + Constructor : function( webAudio, url ){ + this.webAudioList = [ webAudio ]; this.url = url; - this.closed = false; - this.proxy = proxy; - - X_AudioWrapper_updateStates( this, option ); - - if( audio && audio.buffer ){ - this._onDecodeSuccess( audio.buffer ); - } else - if( audio ){ - // TODO 当てにしていたaudioがclose 等した場合 - audio.proxy.listenOnce( 'canplaythrough', this, this._onBufferReady ); - } else { - this.xhr = X.Net.xhrGet( url, 'arraybuffer' ) - .listen( X.Event.PROGRESS, this ) - .listenOnce( [ X.Event.SUCCESS, X.Event.COMPLETE, X.Event.CANCELED ], this ); - }; + this.xhr = X.Net( { 'xhr' : url, 'dataType' : 'arraybuffer' } ) + [ 'listen' ]( X_EVENT_PROGRESS, this ) + [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], 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 : - console.log( 'WebAudio xhr success! ' + !!X_Audio_WebAudio_context.decodeAudioData ); + 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_Audio_WebAudio_context.createBuffer && X_UA[ 'iOS' ] < 8 ){ + this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) ); + } else if( X_Audio_WebAudio_context.decodeAudioData ){ X_Audio_WebAudio_context.decodeAudioData( e.data, this.onDecodeSuccess = X_Callback_create( this, this._onDecodeSuccess ), @@ -92,17 +59,12 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con }; break; - case X.Event.CANCELED : - this.error = 1; - this.proxy.dispatch( 'aborted' ); - break; - - case X.Event.COMPLETE : - this.error = 2; - this.proxy.asyncDispatch( { type : 'error', message : 'xhr error' } ); + case X_EVENT_COMPLETE : + this.error = 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; }, @@ -112,28 +74,23 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con this.onDecodeSuccess && this._onDecodeComplete(); if ( !buffer ) { - this.proxy.asyncDispatch( { type : 'error', message : 'buffer is ' + buffer } ); + this.error = 2; + this[ 'asyncDispatch' ]( X_EVENT_COMPLETE ); return; }; this.buffer = buffer; - this.duration = buffer.duration * 1000; - - this.proxy.asyncDispatch( 'loadedmetadata' ); - this.proxy.asyncDispatch( 'loadeddata' ); - this.proxy.asyncDispatch( 'canplay' ); - this.proxy.asyncDispatch( 'canplaythrough' ); - - this.autoplay && X.Timer.once( 16, this, this.play ); - + + 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.error = 2; + this[ 'asyncDispatch' ]( X_EVENT_COMPLETE ); }, _onDecodeComplete : function(){ @@ -142,36 +99,100 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con X_Callback_correct( this.onDecodeError ); delete this.onDecodeError; }, - - _onBufferReady : function( e ){ - var audio = X_Audio_WebAudio_getBuffer( this.url ); - this._onDecodeSuccess( audio.buffer ); - }, - close : function(){ - delete this.buffer; - - if( this.xhr ) this.xhr.close(); - - if( this.onDecodeSuccess ){ - // 回収はあきらめる、、、 + 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.playing && this.pause(); - this.source && this._sourceDispose(); - - this._onended && X_Callback_correct( this._onended ); - this.gainNode && this.gainNode.disconnect(); - }, + X_Audio_WebAudioWrapper = X_Audio_AbstractAudioBackend[ 'inherits' ]( + 'X.AV.WebAudioWrapper', + X_Class.POOL_OBJECT, + { - _sourceDispose : function(){ - this.source.disconnect(); - delete this.source.onended; - delete this.source; - }, + loader : null, + + _startPos : 0, + _endPosition : 0, + _startTime : 0, + _timerID : 0, + _interval : 0, + buffer : null, + source : null, + gainNode : null, + _onended : null, + + Constructor : function( target, url, option ){ + var i = 0, + l = X_Audio_BUFFER_LIST.length, + loader; + + for( ; i < l; ++i ){ + loader = X_Audio_BUFFER_LIST[ i ]; + if( loader.url === url ){ + this.loader = loader; + loader.webAudioList.push( this ); + break; + }; + }; + + if( !this.loader ){ + this.loader = loader = new X_Audio_BufferLoader( this, url ); + }; + + this.target = target || this; + + this.setState( option ); + + this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, X_WebAudio_handleEvent ); + + if( loader.buffer || loader.error ){ + this._onLoadBufferComplete(); + } else { + loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete ); + }; + }, + _onLoadBufferComplete : function( e ){ + var loader = this.loader, + buffer = loader.buffer; + + e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete ); + + if ( !buffer ) { + this.error = loader.error; + + this.target[ 'dispatch' ]({ + type : X_EVENT_ERROR, + error : loader.error, + message : loader.error === 1 ? + 'load buffer network error' : + 'buffer decode error' + }); + this[ 'kill' ](); + return; + }; + + this.buffer = buffer; + this.duration = buffer.duration * 1000; + + this.target[ 'asyncDispatch' ]( X_EVENT_READY ); + + this.autoplay && X_Timer_once( 16, this, this.play ); + + }, - play : function(){ + actualPlay : function(){ var begin, end; if( !this.buffer ){ @@ -193,17 +214,16 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con this.source.buffer = this.buffer; this.source.connect( this.gainNode ); - this.gainNode.gain.value = this.volume; - - this._timerID && X.Timer.remove( this._timerID ); + this.gainNode.gain.value = this.gain; // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1 - // 破棄された X.Callback が呼ばれて、obj._() でエラーになる。Firefox では、onended は使わない + // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない if( false && this.source.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( end - begin, this, this._onEnded ); + this._timerID && X_Timer_remove( this._timerID ); + this._timerID = X_Timer_once( end - begin, this, this._onEnded ); }; if( this.source.start ){ @@ -212,19 +232,25 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con this.source.noteGrainOn( 0, begin / 1000, end / 1000 ); }; - this.playing = true; - this._startTime = begin; - this._endTime = end; - 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_Audio_WebAudio_context.currentTime * 1000; + this._interval = this._interval || X_Timer_add( 1000, 0, this, this._onInterval ); }, + + _sourceDispose : function(){ + this.source.disconnect(); + delete this.source.onended; + delete this.source; + }, _onInterval : function(){ if( !this.playing ){ delete this._interval; return X_Callback_UN_LISTEN; }; - this.proxy.dispatch( 'timeupdate' ); + this.target[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING ); }, _onEnded : function(){ @@ -232,38 +258,41 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con delete this._timerID; if( this.playing ){ - time = X_Audio_WebAudio_context.currentTime * 1000 - this._playTime - this._endTime + this._startTime | 0; - //console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime ) ) + ' < ' + ( this._endTime - this._startTime ) ); + time = X_Audio_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0; + //console.log( '> onEnd ' + ( this.playing && ( X_Audio_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 ' + time ); - this._timerID = X.Timer.once( -time, this, this._onEnded ); + console.log( '> onEnd crt:' + ( X_Audio_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.looped = true; - this.play(); - this.proxy.dispatch( 'looped' ); + if( this.autoLoop ){ + if( !( this.target[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_Callback_PREVENT_DEFAULT ) ){ + this.looped = true; + this.target[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED ); + this.actualPlay(); + }; } else { - this.pause(); - this.proxy.dispatch( 'ended' ); + this.actualPause(); + this.target[ 'dispatch' ]( X_EVENT_MEDIA_ENDED ); }; }; }, - pause : function(){ + actualPause : function(){ if( !this.playing ) return this; console.log( '[WebAudio] pause' ); - this.seekTime = this.state().currentTime; + this.seekTime = this.getActualCurrentTime(); - this._timerID && X.Timer.remove( this._timerID ); + this._timerID && X_Timer_remove( this._timerID ); delete this._timerID; delete this.playing; @@ -274,55 +303,61 @@ if( !X_UA.iPhone_4s && !X_UA.iPod_2Mini1 && !X_UA.iPod_4 && X_Audio_WebAudio_con this.source.stop( 0 ) : this.source.noteOff( 0 ); }; }, - - state : function( obj ){ - var result; - - if( obj === undefined ){ - return { - startTime : this.startTime, - endTime : this.endTime < 0 ? this.duration : this.endTime, - loopStartTime : this.loopStartTime < 0 ? this.startTime : this.loopStartTime, - loopEndTime : this.loopEndTime < 0 ? ( this.endTime || this.duration ) : this.loopEndTime, - loop : this.loop, - looped : this.looped, - volume : this.volume, - playing : this.playing, - duration : this.duration, - - currentTime : this.playing ? ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime ) : this.seekTime, - error : this.error - }; - }; - result = X_AudioWrapper_updateStates( this, obj ); - - if( result & 2 ){ // seek - this.play(); - } else - if( result & 1 ){ - this.play(); + getActualCurrentTime : function(){ + return X_Audio_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; }; } } ); + + function X_WebAudio_handleEvent( e ){ + switch( e.type ){ + + case X_EVENT_KILL_INSTANCE : + this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete ) + .unregister( this ); + + delete this.buffer; + + this.playing && this.actualPause(); + this.source && this._sourceDispose(); + this._onended && X_Callback_correct( this._onended ); + + this.gainNode && this.gainNode.disconnect(); + break; + }; + }; + + /* + * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1 + * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。 + * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。 + */ + if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] ){ + X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } ); + }; + X_Audio_BACKENDS.push( { backendName : 'Web Audio', + // detect : function( proxy, source, ext ){ - var ok = ext === 'mp3' || ext === 'ogg'; - - proxy.asyncDispatch( ok ? 'support' : 'nosupport' ); + proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } ); }, klass : X_Audio_WebAudioWrapper } ); - };