1 var X_Audio_constructor = 3.1 <= X_UA[ 'Safari' ] && X_UA[ 'Safari' ] < 4 ?
3 //a = document.createElement( 'audio' );
8 // Android1.6 + MobileOpera12 HTMLAudio はいるが呼ぶとクラッシュする
9 !( X_UA[ 'Android' ] < 2 ) ?
10 window[ 'Audio' ] || window.HTMLAudioElement : null,
12 // Blink5 Opera32 Win8 は HTMLAudio が壊れている、WebAudio は mp3 がデコードに失敗、ogg が動作
13 X_Audio_blinkOperaFix = X_UA[ 'BlinkOpera' ] && X_UA[ 'Windows' ],
17 if( X_Audio_constructor ){
18 //http://himaxoff.blog111.fc2.com/blog-entry-97.html
19 //引数なしで new Audio() とすると、Operaでエラーになるそうなので注意。
20 X_TEMP.rawAudio = new X_Audio_constructor( '' );
22 // https://html5experts.jp/miyuki-baba/3766/
23 // TODO Chrome for Android31 で HE-AAC が低速再生されるバグ
24 // TODO Android4 標準ブラウザで ogg のシークが正しくない!
25 if( X_TEMP.rawAudio.canPlayType ){
27 'mp3' : X_TEMP.rawAudio.canPlayType('audio/mpeg'),
28 'opus' : X_TEMP.rawAudio.canPlayType('audio/ogg; codecs="opus"'),
29 'ogg' : X_TEMP.rawAudio.canPlayType('audio/ogg; codecs="vorbis"'),
30 'wav' : X_TEMP.rawAudio.canPlayType('audio/wav; codecs="1"'),
31 'aac' : X_TEMP.rawAudio.canPlayType('audio/aac'),
32 'm4a' : X_TEMP.rawAudio.canPlayType('audio/x-m4a') + X_TEMP.rawAudio.canPlayType('audio/m4a') + X_TEMP.rawAudio.canPlayType('audio/aac'),
33 'mp4' : X_TEMP.rawAudio.canPlayType('audio/x-mp4') + X_TEMP.rawAudio.canPlayType('audio/mp4') + X_TEMP.rawAudio.canPlayType('audio/aac'),
34 'weba' : X_TEMP.rawAudio.canPlayType('audio/webm; codecs="vorbis"')
36 (function( X_Audio_codecs, k, v ){
37 for( k in X_Audio_codecs ){
38 //if( X_EMPTY_OBJECT[ k ] ) continue;
39 v = X_Audio_codecs[ k ];
40 v = v && !!( v.split( 'no' ).join( '' ) );
42 console.log( k + ' ' + X_Audio_codecs[ k ] );
43 X_Audio_codecs[ k ] = true;
45 delete X_Audio_codecs[ k ];
48 if( X_Audio_blinkOperaFix ) delete X_Audio_codecs[ 'mp3' ];
53 'mp3' : X_UA[ 'IE' ] || X_UA[ 'Chrome' ] || ( X_UA[ 'Windows' ] && X_UA[ 'Safari' ] ),
54 'ogg' : 5 <= X_UA[ 'Gecko' ] || X_UA[ 'Chrome' ] || X_UA[ 'Opera' ] ,
55 'wav' : X_UA[ 'Gecko' ] || X_UA[ 'Opera' ] || ( X_UA[ 'Windows' ] && X_UA[ 'Safari' ] ),
56 'aac' : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
57 'm4a' : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
58 'mp4' : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
59 'weba' : 2 <= X_UA[ 'Gecko' ] || 10.6 <= X_UA[ 'Opera' ] // firefox4+(Gecko2+)
61 (function( X_Audio_codecs, k ){
62 for( k in X_Audio_codecs ){
63 //if( X_EMPTY_OBJECT[ k ] ) continue;
64 if( X_Audio_codecs[ k ] ){
65 console.log( k + ' ' + X_Audio_codecs[ k ] );
66 X_Audio_codecs[ k ] = true;
68 delete X_Audio_codecs[ k ];
74 if( X_Audio_blinkOperaFix ){
75 X_Audio_constructor = null;
76 delete X_TEMP.rawAudio;
81 var X_WebAudio_context = // 4s 以下ではない iPad 2G または iPad mini 1G 以下ではない, iPod touch 4G 以下ではない
82 !X_UA[ 'iPhone_4s' ] && !X_UA[ 'iPad_2Mini1' ] && !X_UA[ 'iPod_4' ] &&
83 // iOS7 以上で HTML Audio が鳴らない問題を見ていくよ
85 // Android2 + Gecko で WebAudio が極めて不安定
86 !( X_UA[ 'Fennec' ] && X_UA[ 'Android' ] < 3 ) &&
87 // AOSP でも WebAudio を不完全に実装するものがある, touch の有無も不明のため一律に切ってしまう
88 !X_UA[ 'AOSP' ] && !( X_UA[ 'ChromeWV' ] < 5 ) &&
89 // Blink HTMLAudio 調査用
91 // Firefox40.0.5 + Windows8 で音声が途中から鳴らなくなる
92 // Firefox41.0.1 + Windows8 で音声が途中から鳴らなくなる
93 !( 40 <= X_UA[ 'Gecko' ] && X_UA[ 'Gecko' ] < 44 && X_UA[ 'Windows' ] ) &&
94 ( window[ 'AudioContext' ] || window[ 'webkitAudioContext' ] ),
95 X_WebAudio_BUFFER_LIST = [],
96 X_WebAudio_need1stTouch = X_UA[ 'iOS' ],
97 X_WebAudio_touchState = X_WebAudio_need1stTouch,
99 X_WebAudio_BufferLoader,
103 * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
105 if( X_WebAudio_context ){
107 X_WebAudio_context = new X_WebAudio_context;
109 X_WebAudio_BufferLoader = X_EventDispatcher[ 'inherits' ](
110 'X.WebAudio.BufferLoader',
115 onDecodeSuccess : null,
116 onDecodeError : null,
122 'Constructor' : function( webAudio, url ){
123 this.webAudioList = [ webAudio ];
125 this.xhr = X[ 'Net' ]( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
126 [ 'listen' ]( X_EVENT_PROGRESS, this )
127 [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
128 X_WebAudio_BUFFER_LIST.push( this );
131 handleEvent : function( e ){
135 case X_EVENT_PROGRESS :
136 for( i = 0, l = this.webAudioList.length; i < l; ++i ){
137 this.webAudioList[ i ][ 'dispatch' ]( { type : X_EVENT_PROGRESS, 'percent' : e[ 'percent' ] } );
141 case X_EVENT_SUCCESS :
143 // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
145 // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
146 // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
147 // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
148 // ただし iOS 8.1.2 では エラーになる
149 if( X_UA[ 'iOS' ] < 8 || !X_WebAudio_context[ 'decodeAudioData' ] ){
150 this._onDecodeSuccess( X_WebAudio_context[ 'createBuffer' ]( e.response, false ) );
152 if( X_WebAudio_context[ 'decodeAudioData' ] ){
153 X_WebAudio_context[ 'decodeAudioData' ]( e.response,
154 this.onDecodeSuccess = X_Closure_create( this, this._onDecodeSuccess ),
155 this.onDecodeError = X_Closure_create( this, this._onDecodeError ) );
159 case X_EVENT_COMPLETE :
161 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
164 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
168 _onDecodeSuccess : function( buffer ){
169 this.onDecodeSuccess && this._onDecodeComplete();
173 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
177 console.log( 'WebAudio decode success!' );
179 this.audioBuffer = buffer;
181 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
183 console.log( 'WebAudio decoded!' );
186 _onDecodeError : function(){
187 console.log( 'WebAudio decode error!' );
188 this._onDecodeComplete();
190 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
193 _onDecodeComplete : function(){
194 X_Closure_correct( this.onDecodeSuccess );
195 delete this.onDecodeSuccess;
196 X_Closure_correct( this.onDecodeError );
197 delete this.onDecodeError;
200 unregister : function( webAudio ){
201 var list = this.webAudioList,
202 i = list.indexOf( webAudio );
207 this.xhr && this.xhr[ 'kill' ]();
217 X_WebAudio = X_AudioBase[ 'inherits' ](
234 'Constructor' : function( disatcher, url, option ){
236 l = X_WebAudio_BUFFER_LIST.length,
240 * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
241 * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
242 * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
244 if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] && !X_WebAudio_fpsFix ){
245 X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } );
246 X_WebAudio_fpsFix = true;
250 loader = X_WebAudio_BUFFER_LIST[ i ];
251 if( loader.audioUrl === url ){
252 this.loader = loader;
253 loader.webAudioList.push( this );
259 this.loader = loader = X_WebAudio_BufferLoader( this, url );
262 this.disatcher = disatcher || this;
263 this.setState( option );
265 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
267 if( loader.audioBuffer || loader.errorState ){
268 this._onLoadBufferComplete();
270 loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
275 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
278 delete this.audioBuffer;
280 this.playing && this.actualPause();
281 this.bufferSource && this._sourceDispose();
283 this._onended && X_Closure_correct( this._onended );
285 this.gainNode && this.gainNode.disconnect();
287 _onLoadBufferComplete : function( e ){
288 var loader = this.loader,
289 buffer = loader.audioBuffer;
291 e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
294 this.error = loader.errorState;
295 this.disatcher[ 'dispatch' ]({
296 type : X_EVENT_ERROR,
297 error : loader.errorState,
298 message : loader.errorState === 1 ?
299 'load buffer network error' :
300 'buffer decode error'
306 this.audioBuffer = buffer;
307 this.duration = buffer.duration * 1000;
309 this.disatcher[ 'asyncDispatch' ]( X_WebAudio_touchState ? X_EVENT_MEDIA_TOUCH_FOR_LOAD : X_EVENT_READY );
312 actualPlay : function(){
315 console.log( '[WebAudio] play abuf:' + !!this.audioBuffer );
317 if( !this.audioBuffer ){
318 this._playReserved = true;
322 if( X_WebAudio_touchState ){
323 e = X_EventDispatcher_CURRENT_EVENTS[ X_EventDispatcher_CURRENT_EVENTS.length - 1 ];
324 if( !e || !e[ 'pointerType' ] ){
325 // alert( 'タッチイベント以外での play! ' + ( e ? e.type : '' ) );
328 // http://qiita.com/uupaa/items/e5856e3cb2a9fc8c5507
329 // iOS9 + touchstart で呼んでいた場合、 X_ViewPort['listenOnce']('pointerup',this,this.actualPlay())
330 this.disatcher[ 'asyncDispatch' ]( X_EVENT_READY );
332 X_WebAudio_touchState = false;
334 end = X_Audio_getEndTime( this );
335 begin = X_Audio_getStartTime( this, end, true );
337 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
339 if( this.bufferSource ) this._sourceDispose();
340 if( !this.gainNode ){
341 this.gainNode = X_WebAudio_context[ 'createGain' ] ? X_WebAudio_context[ 'createGain' ]() : X_WebAudio_context[ 'createGainNode' ]();
342 this.gainNode[ 'connect' ]( X_WebAudio_context[ 'destination' ] );
344 this.bufferSource = X_WebAudio_context[ 'createBufferSource' ]();
345 this.bufferSource.buffer = this.audioBuffer;
346 this.bufferSource[ 'connect' ]( this.gainNode );
348 this.gainNode[ 'gain' ].value = this.gain;
350 // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
351 // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
352 // 多くのブラウザで onended は timer を使ったカウントより遅いので使わない
353 //if( this.bufferSource.onended !== undefined ){
354 //console.log( '> use onended' );
355 //this.bufferSource.onended = this._onended || ( this._onended = X_Closure_create( this, this._onEnded ) );
357 this._timerID && X_Timer_remove( this._timerID );
358 this._timerID = X_Timer_once( end - begin, this, this._onEnded );
361 if( this.bufferSource.start ){
362 this.bufferSource.start( 0, begin / 1000, end / 1000 );
364 this.bufferSource[ 'noteGrainOn' ]( 0, begin / 1000, end / 1000 );
368 this._startPos = begin;
369 this._endPosition = end;
370 this._startTime = X_WebAudio_context.currentTime * 1000;
371 this._interval = this._interval || X_Timer_add( 1000, 0, this, this._onInterval );
374 _sourceDispose : function(){
375 this.bufferSource.disconnect();
376 //delete this.bufferSource.onended;
377 delete this.bufferSource;
380 _onInterval : function(){
382 delete this._interval;
383 return X_CALLBACK_UN_LISTEN;
385 this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
388 _onEnded : function(){
390 delete this._timerID;
393 time = X_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
394 //console.log( '> onEnd ' + ( this.playing && ( X_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
397 if( time < 0 ) return;
400 //console.log( '> onEnd crt:' + ( X_WebAudio_context.currentTime * 1000 ) + ' startTime:' + this._startTime +
401 // ' from:' + this._startPos + ' to:' + this._endPosition );
402 this._timerID = X_Timer_once( -time, this, this._onEnded );
408 if( !( this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
410 this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
415 this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
420 actualPause : function(){
421 console.log( '[WebAudio] pause' );
423 this._timerID && X_Timer_remove( this._timerID );
424 delete this._timerID;
427 if( this.bufferSource ){
428 //if( this.bufferSource.onended ) delete this.bufferSource.onended;
430 this.bufferSource.stop ?
431 this.bufferSource.stop( 0 ) : this.bufferSource[ 'noteOff' ]( 0 );
435 getActualCurrentTime : function(){
436 return X_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
439 afterUpdateState : function( result ){
440 if( result & 2 || result & 1 ){ // seek
444 this.gainNode[ 'gain' ].value = this.gain;
451 X_Audio_BACKENDS.push(
455 backendName : 'WebAudio',
457 canPlay : X_Audio_codecs,
459 detect : function( proxy, source, ext ){
460 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );