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_BUFFER_LIST = [],
6 X_Audio_WebAudioWrapper;
9 * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
11 if( X_Audio_WebAudio_context ){
13 X_Audio_WebAudio_context = new X_Audio_WebAudio_context;
15 X_Audio_BufferLoader = X_EventDispatcher[ 'inherits' ](
16 'X.AV.WebAudioBufferLoader',
21 onDecodeSuccess : null,
28 Constructor : function( webAudio, url ){
29 this.webAudioList = [ webAudio ];
31 this.xhr = X.Net( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
32 [ 'listen' ]( X_EVENT_PROGRESS, this )
33 [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
36 handleEvent : function( e ){
38 case X_EVENT_PROGRESS :
39 this[ 'dispatch' ]( { type : 'progress', 'percent' : e[ 'percent' ] } );
42 case X_EVENT_SUCCESS :
44 // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
46 // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
47 // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
48 // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
49 // ただし iOS 8.1.2 では エラーになる
50 if( X_Audio_WebAudio_context.createBuffer && X_UA[ 'iOS' ] < 8 ){
51 this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) );
53 if( X_Audio_WebAudio_context.decodeAudioData ){
54 X_Audio_WebAudio_context.decodeAudioData( e.data,
55 this.onDecodeSuccess = X_Callback_create( this, this._onDecodeSuccess ),
56 this.onDecodeError = X_Callback_create( this, this._onDecodeError ) );
58 this._onDecodeSuccess( X_Audio_WebAudio_context.createBuffer( e.data, false ) );
62 case X_EVENT_COMPLETE :
64 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
67 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
71 _onDecodeSuccess : function( buffer ){
72 console.log( 'WebAudio decode success!' );
74 this.onDecodeSuccess && this._onDecodeComplete();
78 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
84 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
86 console.log( 'WebAudio decoded!' );
89 _onDecodeError : function(){
90 console.log( 'WebAudio decode error!' );
91 this._onDecodeComplete();
93 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
96 _onDecodeComplete : function(){
97 X_Callback_correct( this.onDecodeSuccess );
98 delete this.onDecodeSuccess;
99 X_Callback_correct( this.onDecodeError );
100 delete this.onDecodeError;
103 unregister : function( webAudio ){
104 var list = this.webAudioList,
105 i = list.indexOf( webAudio );
109 this.xhr && this.xhr[ 'kill' ]();
119 X_Audio_WebAudioWrapper = X_Audio_AbstractAudioBackend[ 'inherits' ](
120 'X.AV.WebAudioWrapper',
136 Constructor : function( target, url, option ){
138 l = X_Audio_BUFFER_LIST.length,
142 loader = X_Audio_BUFFER_LIST[ i ];
143 if( loader.url === url ){
144 this.loader = loader;
145 loader.webAudioList.push( this );
151 this.loader = loader = new X_Audio_BufferLoader( this, url );
154 this.target = target || this;
156 this.setState( option );
158 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, X_WebAudio_handleEvent );
160 if( loader.buffer || loader.error ){
161 this._onLoadBufferComplete();
163 loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
166 _onLoadBufferComplete : function( e ){
167 var loader = this.loader,
168 buffer = loader.buffer;
170 e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
173 this.error = loader.error;
175 this.target[ 'dispatch' ]({
176 type : X_EVENT_ERROR,
177 error : loader.error,
178 message : loader.error === 1 ?
179 'load buffer network error' :
180 'buffer decode error'
186 this.buffer = buffer;
187 this.duration = buffer.duration * 1000;
189 this.target[ 'asyncDispatch' ]( X_EVENT_READY );
191 this.autoplay && X_Timer_once( 16, this, this.play );
195 actualPlay : function(){
199 this.autoplay = true;
203 end = X_AudioWrapper_getEndTime( this );
204 begin = X_AudioWrapper_getStartTime( this, end, true );
206 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
208 if( this.source ) this._sourceDispose();
209 if( !this.gainNode ){
210 this.gainNode = X_Audio_WebAudio_context.createGain ? X_Audio_WebAudio_context.createGain() : X_Audio_WebAudio_context.createGainNode();
211 this.gainNode.connect( X_Audio_WebAudio_context.destination );
213 this.source = X_Audio_WebAudio_context.createBufferSource();
214 this.source.buffer = this.buffer;
215 this.source.connect( this.gainNode );
217 this.gainNode.gain.value = this.gain;
219 // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
220 // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
221 if( false && this.source.onended !== undefined ){
222 //console.log( '> use onended' );
223 this.source.onended = this._onended || ( this._onended = X_Callback_create( this, this._onEnded ) );
225 this._timerID && X_Timer_remove( this._timerID );
226 this._timerID = X_Timer_once( end - begin, this, this._onEnded );
229 if( this.source.start ){
230 this.source.start( 0, begin / 1000, end / 1000 );
232 this.source.noteGrainOn( 0, begin / 1000, end / 1000 );
236 this._startPos = begin;
237 this._endPosition = end;
238 this._startTime = X_Audio_WebAudio_context.currentTime * 1000;
239 this._interval = this._interval || X_Timer_add( 1000, 0, this, this._onInterval );
242 _sourceDispose : function(){
243 this.source.disconnect();
244 delete this.source.onended;
248 _onInterval : function(){
250 delete this._interval;
251 return X_Callback_UN_LISTEN;
253 this.target[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
256 _onEnded : function(){
258 delete this._timerID;
261 time = X_Audio_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
262 //console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
265 if( time < 0 ) return;
268 console.log( '> onEnd crt:' + ( X_Audio_WebAudio_context.currentTime * 1000 ) + ' startTime:' + this._startTime +
269 ' from:' + this._startPos + ' to:' + this._endPosition );
270 this._timerID = X_Timer_once( -time, this, this._onEnded );
276 if( !( this.target[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_Callback_PREVENT_DEFAULT ) ){
278 this.target[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
283 this.target[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
288 actualPause : function(){
289 if( !this.playing ) return this;
291 console.log( '[WebAudio] pause' );
293 this.seekTime = this.getActualCurrentTime();
295 this._timerID && X_Timer_remove( this._timerID );
296 delete this._timerID;
300 if( this.source.onended ) delete this.source.onended;
303 this.source.stop( 0 ) : this.source.noteOff( 0 );
307 getActualCurrentTime : function(){
308 return X_Audio_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
311 afterUpdateState : function( result ){
312 if( result & 2 || result & 1 ){ // seek
316 this.gainNode.gain.value = this.gain;
323 function X_WebAudio_handleEvent( e ){
326 case X_EVENT_KILL_INSTANCE :
327 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
332 this.playing && this.actualPause();
333 this.source && this._sourceDispose();
335 this._onended && X_Callback_correct( this._onended );
337 this.gainNode && this.gainNode.disconnect();
343 * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
344 * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
345 * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
347 if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] ){
348 X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } );
351 X_Audio_BACKENDS.push(
353 backendName : 'Web Audio',
356 detect : function( proxy, source, ext ){
357 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
360 klass : X_Audio_WebAudioWrapper