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._() でエラーになる。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._startTime = begin;
231 this._playTime = 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._playTime - this._endTime + this._startTime | 0;
249 //console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime ) ) + ' < ' + ( this._endTime - this._startTime ) );
252 if( time < 0 ) return;
255 console.log( '> onEnd ' + ( -time ) + ' start:' + this._startTime + '-' + this._endTime );
256 this._timerID = X.Timer.once( -time, this, this._onEnded );
262 if( !( this.proxy.dispatch( X.Event.MEDIA_BEFORE_LOOP ) & X.Callback.PREVENT_DEFAULT ) ){
264 this.proxy.dispatch( X.Event.MEDIA_LOOPED );
269 this.proxy.dispatch( X.Event.MEDIA_ENDED );
275 if( !this.playing ) return this;
277 console.log( '[WebAudio] pause' );
279 this.seekTime = this.state().currentTime;
281 this._timerID && X.Timer.remove( this._timerID );
282 delete this._timerID;
286 if( this.source.onended ) delete this.source.onended;
289 this.source.stop( 0 ) : this.source.noteOff( 0 );
293 state : function( obj ){
296 if( obj === undefined ){
298 startTime : this.startTime,
299 endTime : this.endTime < 0 ? this.duration : this.endTime,
300 loopStartTime : this.loopStartTime < 0 ? this.startTime : this.loopStartTime,
301 loopEndTime : this.loopEndTime < 0 ? ( this.endTime || this.duration ) : this.loopEndTime,
303 looped : this.looped,
304 volume : this.volume,
305 playing : this.playing,
306 duration : this.duration,
308 currentTime : this.playing ? ( X_Audio_WebAudio_context.currentTime * 1000 - this._playTime + this._startTime | 0 ) : this.seekTime,
313 result = X_AudioWrapper_updateStates( this, obj );
315 if( result & 2 || result & 1 ){ // seek
319 this.gainNode.gain.value = this.volume;
327 X_Audio_BACKENDS.push(
329 backendName : 'Web Audio',
332 detect : function( proxy, source, ext ){
333 proxy.asyncDispatch( X_Audio_codecs[ ext ] ? X_Audio_CAN_PLAY : X_Audio_NOT_PLAY );
336 klass : X_Audio_WebAudioWrapper