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 // WebAudioAPIを使っているはずなのに、マナーモードで音が出る!?
18 // http://qiita.com/gonshi_com/items/e41dbb80f5eb4c176108
19 // HTML Audio、もしくはHTML Videoをページ内で1つでも使用していた場合、そのページでは WebAudioAPI の音がマナーモード時にも鳴ってしまう
21 if( X_Audio_constructor ){
22 //http://himaxoff.blog111.fc2.com/blog-entry-97.html
23 //引数なしで new Audio() とすると、Operaでエラーになるそうなので注意。
24 X_TEMP.rawAudio = new X_Audio_constructor( '' );
26 // https://html5experts.jp/miyuki-baba/3766/
27 // TODO Chrome for Android31 で HE-AAC が低速再生されるバグ
28 // TODO Android4 標準ブラウザで ogg のシークが正しくない!
29 if( X_TEMP.rawAudio.canPlayType ){
31 'mp3' : X_TEMP.rawAudio.canPlayType('audio/mpeg'),
32 'opus' : X_TEMP.rawAudio.canPlayType('audio/ogg; codecs="opus"'),
33 'ogg' : X_TEMP.rawAudio.canPlayType('audio/ogg; codecs="vorbis"'),
34 'wav' : X_TEMP.rawAudio.canPlayType('audio/wav; codecs="1"'),
35 'aac' : X_TEMP.rawAudio.canPlayType('audio/aac'),
36 'm4a' : X_TEMP.rawAudio.canPlayType('audio/x-m4a') + X_TEMP.rawAudio.canPlayType('audio/m4a') + X_TEMP.rawAudio.canPlayType('audio/aac'),
37 'mp4' : X_TEMP.rawAudio.canPlayType('audio/x-mp4') + X_TEMP.rawAudio.canPlayType('audio/mp4') + X_TEMP.rawAudio.canPlayType('audio/aac'),
38 'weba' : X_TEMP.rawAudio.canPlayType('audio/webm; codecs="vorbis"')
40 (function( X_Audio_codecs, k, v ){
41 for( k in X_Audio_codecs ){
42 //if( X_EMPTY_OBJECT[ k ] ) continue;
43 v = X_Audio_codecs[ k ];
44 v = v && !!( v.split( 'no' ).join( '' ) );
46 console.log( k + ' ' + X_Audio_codecs[ k ] );
47 X_Audio_codecs[ k ] = true;
49 delete X_Audio_codecs[ k ];
52 if( X_Audio_blinkOperaFix ) delete X_Audio_codecs[ 'mp3' ];
57 'mp3' : X_UA[ 'IE' ] || X_UA[ 'Chrome' ] || ( X_UA[ 'Windows' ] && X_UA[ 'Safari' ] ),
58 'ogg' : 5 <= X_UA[ 'Gecko' ] || X_UA[ 'Chrome' ] || X_UA[ 'Opera' ] ,
59 'wav' : X_UA[ 'Gecko' ] || X_UA[ 'Opera' ] || ( X_UA[ 'Windows' ] && X_UA[ 'Safari' ] ),
60 'aac' : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
61 'm4a' : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
62 'mp4' : X_UA[ 'IE' ] || X_UA[ 'WebKit' ],
63 'weba' : 2 <= X_UA[ 'Gecko' ] || 10.6 <= X_UA[ 'Opera' ] // firefox4+(Gecko2+)
65 (function( X_Audio_codecs, k ){
66 for( k in X_Audio_codecs ){
67 //if( X_EMPTY_OBJECT[ k ] ) continue;
68 if( X_Audio_codecs[ k ] ){
69 console.log( k + ' ' + X_Audio_codecs[ k ] );
70 X_Audio_codecs[ k ] = true;
72 delete X_Audio_codecs[ k ];
78 if( X_Audio_blinkOperaFix ){
79 X_Audio_constructor = null;
80 delete X_TEMP.rawAudio;
85 var X_WebAudio_Context = // 4s 以下ではない iPad 2G または iPad mini 1G 以下ではない, iPod touch 4G 以下ではない
86 !X_UA[ 'iPhone_4s' ] && !X_UA[ 'iPad_2Mini1' ] && !X_UA[ 'iPod_4' ] &&
87 // iOS7 以上で HTML Audio が鳴らない問題を見ていくよ
89 // Android2 + Gecko で WebAudio が極めて不安定
90 !( X_UA[ 'Fennec' ] && X_UA[ 'Android' ] < 3 ) &&
91 // AOSP でも WebAudio を不完全に実装するものがある, touch の有無も不明のため一律に切ってしまう
92 !X_UA[ 'AOSP' ] && !( X_UA[ 'ChromeWV' ] < 5 ) &&
93 // Blink HTMLAudio 調査用
95 // Firefox40.0.5 + Windows8 で音声が途中から鳴らなくなる
96 // Firefox41.0.1 + Windows8 で音声が途中から鳴らなくなる
97 !( 40 <= X_UA[ 'Gecko' ] && /* X_UA[ 'Gecko' ] < 48 && */ X_UA[ 'Windows' ] ) &&
98 ( window[ 'AudioContext' ] || window[ 'webkitAudioContext' ] || window[ 'mozAudioContext' ] ),
100 X_WebAudio_BUFFER_LIST = [],
101 X_WebAudio_need1stTouch = X_UA[ 'iOS' ],
102 X_WebAudio_isNoTouch = X_WebAudio_need1stTouch,
103 X_WebAudio_needRateFix = X_WebAudio_need1stTouch,
105 X_WebAudio_BufferLoader,
109 * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
111 if( X_WebAudio_Context ){
113 X_WebAudio_context = new X_WebAudio_Context;
115 // http://lilting.ch/3323.html
116 // 【間に合わせ】iOS9系でのWebAudioの音割れ対処について
118 if( X_WebAudio_needRateFix ){
119 X_WebAudio_context.close();
120 X_WebAudio_context = new X_WebAudio_Context;
125 * http://qiita.com/simiraaaa/items/79a9ac972cc76fb58d93
126 * [WebAudio API] iOS9で音が歪む、遅い、低い、割れる等の回避方法
128 if( X_WebAudio_needRateFix ){
129 X_TEMP.webAudioSampleRateFix = function( sampleRate ){
130 X_TEMP.webAudioDummyPlay( sampleRate );
132 if( true || X_WebAudio_context[ 'sampleRate' ] !== sampleRate ){
133 // alert( '[debug]iOSで音割れを検知、修復コードを実施 ctxSR:' + X_WebAudio_context[ 'sampleRate' ] + ' abfSR:' + sampleRate );
135 X_WebAudio_context.close && X_WebAudio_context.close();
136 X_WebAudio_context = new X_WebAudio_Context;
138 X_TEMP.webAudioDummyPlay( sampleRate );
141 delete X_TEMP.webAudioSampleRateFix;
142 delete X_TEMP.webAudioDummyPlay;
144 X_TEMP.webAudioDummyPlay = function( sampleRate, source ){
145 source = X_WebAudio_context[ 'createBufferSource' ]();
146 source.buffer = X_WebAudio_context[ 'createBuffer' ]( 1, 1, sampleRate );
147 source[ 'connect' ]( X_WebAudio_context[ 'destination' ] );
148 source.start ? source.start( 0 ) : source[ 'noteOn' ] ? source[ 'noteOn' ]( 0 ) : source[ 'noteGrainOn' ]( 0 );
152 X_WebAudio_BufferLoader = X_EventDispatcher[ 'inherits' ](
153 'X.WebAudio.BufferLoader',
158 onDecodeSuccess : null,
159 onDecodeError : null,
165 'Constructor' : function( webAudio, url ){
166 this.webAudioList = [ webAudio ];
168 this.xhr = X[ 'Net' ]( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
169 [ 'listen' ]( X_EVENT_PROGRESS, this )
170 [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
171 X_WebAudio_BUFFER_LIST.push( this );
174 handleEvent : function( e ){
178 case X_EVENT_PROGRESS :
179 for( i = 0, l = this.webAudioList.length; i < l; ++i ){
180 this.webAudioList[ i ][ 'dispatch' ]( { type : X_EVENT_PROGRESS, 'percent' : e[ 'percent' ] } );
184 case X_EVENT_SUCCESS :
186 // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
188 // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
189 // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
190 // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
191 // ただし iOS 8.1.2 では エラーになる
192 if( X_UA[ 'iOS' ] < 8 || !X_WebAudio_context[ 'decodeAudioData' ] ){
193 this._onDecodeSuccess( X_WebAudio_context[ 'createBuffer' ]( e.response, false ) );
195 if( X_WebAudio_context[ 'decodeAudioData' ] ){
196 X_WebAudio_context[ 'decodeAudioData' ]( e.response,
197 this.onDecodeSuccess = X_Closure_create( this, this._onDecodeSuccess ),
198 this.onDecodeError = X_Closure_create( this, this._onDecodeError ) );
202 case X_EVENT_COMPLETE :
204 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
207 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
211 _onDecodeSuccess : function( buffer ){
212 this.onDecodeSuccess && this._onDecodeComplete();
216 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
220 console.log( 'WebAudio decode success!' );
222 this.audioBuffer = buffer;
224 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
226 console.log( 'WebAudio decoded!' );
229 _onDecodeError : function(){
230 console.log( 'WebAudio decode error!' );
231 this._onDecodeComplete();
233 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
236 _onDecodeComplete : function(){
237 X_Closure_correct( this.onDecodeSuccess );
238 delete this.onDecodeSuccess;
239 X_Closure_correct( this.onDecodeError );
240 delete this.onDecodeError;
243 unregister : function( webAudio ){
244 var list = this.webAudioList,
245 i = list.indexOf( webAudio );
250 this.xhr && this.xhr[ 'kill' ]();
259 X_WebAudio = X_AudioBase[ 'inherits' ](
279 'Constructor' : function( dispatcher, url, option ){
281 l = X_WebAudio_BUFFER_LIST.length,
285 * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
286 * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
287 * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
289 if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] && !X_WebAudio_fpsFix ){
290 X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } );
291 X_WebAudio_fpsFix = true;
295 loader = X_WebAudio_BUFFER_LIST[ i ];
296 if( loader.audioUrl === url ){
297 this.loader = loader;
298 loader.webAudioList.push( this );
304 this.loader = loader = X_WebAudio_BufferLoader( this, url );
307 this.dispatcher = dispatcher || this;
308 this.setState( option );
310 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
312 if( loader.audioBuffer || loader.errorState ){
313 this._onLoadBufferComplete();
315 loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
318 if( X_WebAudio_isNoTouch ){
319 X_TEMP.xWebAudioInstances = X_TEMP.xWebAudioInstances || [];
320 X_TEMP.xWebAudioInstances.push( this );
325 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
328 delete this.audioBuffer;
330 this.playing && this.actualPause();
331 this.bufferSource && this._sourceDispose();
333 //this._onended && X_Closure_correct( this._onended );
335 this.gainNode && this.gainNode.disconnect();
337 if( X_WebAudio_isNoTouch ){
338 X_TEMP.xWebAudioInstances.splice( X_TEMP.xWebAudioInstances.indexOf( this ), 1 );
341 _onLoadBufferComplete : function( e ){
342 var loader = this.loader,
343 buffer = loader.audioBuffer;
345 e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
348 this.error = loader.errorState;
349 this.dispatcher[ 'dispatch' ]({
350 type : X_EVENT_ERROR,
351 error : loader.errorState,
352 message : loader.errorState === 1 ?
353 'load buffer network error' :
354 'buffer decode error'
360 this.audioBuffer = buffer;
361 this.duration = buffer.duration * 1000;
363 this.dispatcher[ 'asyncDispatch' ]( X_WebAudio_isNoTouch ? X_EVENT_MEDIA_WAIT_FOR_TOUCH : X_EVENT_READY );
366 actualPlay : function(){
367 var xWebAudio, begin, end;
369 console.log( '[WebAudio] play abuf:' + !!this.audioBuffer );
371 if( !this.audioBuffer ){
372 this._playReserved = true;
376 if( X_WebAudio_isNoTouch ){
378 var e = X_EventDispatcher_CURRENT_EVENTS[ X_EventDispatcher_CURRENT_EVENTS.length - 1 ];
379 if( !e || !e[ 'pointerType' ] ){
380 // alert( 'タッチイベント以外での play! ' + ( e ? e.type : '' ) );
385 // http://qiita.com/uupaa/items/e5856e3cb2a9fc8c5507
386 // iOS9 + touchstart で呼んでいた場合、 X_ViewPort['listenOnce']('pointerup',this,this.actualPlay())
387 this.dispatcher[ 'asyncDispatch' ]( X_EVENT_READY );
389 // Web Audio インスタンスが複数生成された場合、一つの Web Audio に対してタッチによる play が開始されたら、
390 // 1. 同時に生成された他の X.Audio インスタンスに対して READY イベントを発する
391 // 2. 以降の X.Audio インスタンス生成時に needTouchForPlay フラグは false
392 // ちなみに HTML Audio インスタンスは各々にタッチが必要
393 //X_WebAudio_isNoTouch = false;
395 while( xWebAudio = X_TEMP.xWebAudioInstances.pop() ){
396 xWebAudio !== this && xWebAudio[ 'asyncDispatch' ]( X_EVENT_READY );
398 delete X_TEMP.xWebAudioInstances;
400 X_WebAudio_isNoTouch = false;
402 X_TEMP.webAudioSampleRateFix && X_TEMP.webAudioSampleRateFix( this.audioBuffer[ 'sampleRate' ] );
405 end = X_Audio_getEndTime( this );
406 begin = X_Audio_getStartTime( this, end, true );
408 console.log( '[WebAudio] play ' + begin + ' -> ' + end + ' loop: ' + this.autoLoop + ' :' + this.loopStartTime + ' -> ' + this.loopEndTime );
409 this._createTree( begin, end );
411 /* win8.1 Firefox45, win8.1 Chrome48 で動かなくなる...
412 if( this.bufferSource[ 'loop' ] = this.autoLoop ){
413 this.bufferSource[ 'loopStart' ] = 0 <= this.loopStartTime ? this.loopStartTime / 1000 : begin / 1000;
414 this.bufferSource[ 'loopEnd' ] = 0 <= this.loopEndTime ? this.loopEndTime / 1000 : end / 1000;
417 // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
418 // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
419 // 多くのブラウザで onended は timer を使ったカウントより遅いので使わない
420 //if( this.bufferSource.onended !== undefined ){
421 //console.log( '> use onended' );
422 //this.bufferSource.onended = this._onended || ( this._onended = X_Closure_create( this, this._onEnded ) );
424 //this._timerID && X_Timer_remove( this._timerID );
425 //this._timerID = X_Timer_once( end - begin, this, this._onEnded );
429 this._startPos = begin;
430 this._endPosition = end;
431 this._startTime = X_WebAudio_context.currentTime * 1000;
432 this._interval = this._interval || X_Timer_add( 100, 0, this, this._onInterval );
435 _createTree : function( begin, end ){
436 if( this.bufferSource ) this._sourceDispose();
438 if( !this.gainNode ){
439 this.gainNode = X_WebAudio_context[ 'createGain' ] ? X_WebAudio_context[ 'createGain' ]() : X_WebAudio_context[ 'createGainNode' ]();
440 this.gainNode[ 'connect' ]( X_WebAudio_context[ 'destination' ] );
443 this.bufferSource = X_WebAudio_context[ 'createBufferSource' ]();
444 this.bufferSource.buffer = this.audioBuffer;
445 this.bufferSource[ 'connect' ]( this.gainNode );
447 this.gainNode[ 'gain' ].value = this.gain;
449 if( !this.bufferPlay ){
450 this.bufferPlay = this.bufferSource.start ? 'start' : this.bufferSource[ 'noteOn' ] ? 'noteOn' : 'noteGrainOn';
451 this.bufferStop = this.bufferSource.stop ? 'stop' : 'noteOff';
453 // https://developer.mozilla.org/ja/docs/Web/API/AudioBufferSourceNode
454 // AudioBufferSourceNode.start()の呼び出しは一度しかできません。
455 this.bufferSource[ this.bufferPlay ]( 0, begin / 1000, ( end - begin ) / 1000 );
458 _sourceDispose : function(){
459 this.bufferSource.disconnect();
460 //delete this.bufferSource.onended;
461 delete this.bufferSource;
464 _onInterval : function(){
468 // TODO 再生中に終了時間だけ変えた場合!
469 time = X_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
470 //console.log( '> onEnd ' + ( this.playing && ( X_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
473 this.dispatcher[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
478 if( !( this.dispatcher[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
480 this.dispatcher[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
483 delete this._interval;
484 return X_CALLBACK_UN_LISTEN;
488 this.dispatcher[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
493 actualPause : function(){
494 console.log( '[WebAudio] pause' );
496 this._interval && X_Timer_remove( this._interval );
497 delete this._interval;
500 if( this.bufferSource ){
501 this.bufferSource[ this.bufferStop ]( 0 );
505 getActualCurrentTime : function(){
506 return X_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
509 afterUpdateState : function( result ){
510 if( result & 2 || result & 1 ){ // seek
514 this.gainNode[ 'gain' ].value = this.gain;
521 X_Audio_BACKENDS.push(
525 backendName : 'WebAudio',
527 canPlay : X_Audio_codecs,
529 detect : function( proxy, ext /* hash */ ){
530 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );