OSDN Git Service

67011bb02df4df21586a70aebdd84dd3162f60f4
[pettanr/clientJs.git] / 0.6.x / js / 07_audio / 01_XWebAudio.js
1 var X_Audio_constructor = 3.1 <= X_UA[ 'Safari' ] && X_UA[ 'Safari' ] < 4 ?
2                                                                 function( s, a ){
3                                                                         //a = document.createElement( 'audio' );
4                                                                         //a.src = s;
5                                                                         //a.load();
6                                                                         return a;
7                                                                 } :
8                                                 // Android1.6 + MobileOpera12 HTMLAudio はいるが呼ぶとクラッシュする
9                                                   !( X_UA[ 'Android' ] < 2 ) ?
10                                                                 window[ 'Audio' ] || window.HTMLAudioElement : null,
11         
12         // Blink5 Opera32 Win8 は HTMLAudio が壊れている、WebAudio は mp3 がデコードに失敗、ogg が動作
13         X_Audio_blinkOperaFix = X_UA[ 'BlinkOpera' ] && X_UA[ 'Windows' ],
14
15         X_Audio_codecs;
16
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( '' );
21         
22         // https://html5experts.jp/miyuki-baba/3766/
23         // TODO Chrome for Android31 で HE-AAC が低速再生されるバグ
24         // TODO Android4 標準ブラウザで ogg のシークが正しくない!
25         if( X_TEMP.rawAudio.canPlayType ){
26                 X_Audio_codecs = {
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"')
35                 };
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( '' ) );
41                                 if( v ){
42                                         console.log( k + ' ' + X_Audio_codecs[ k ] );
43                                         X_Audio_codecs[ k ] = true;
44                                 } else {
45                                         delete X_Audio_codecs[ k ];
46                                 };
47                         };
48                         if( X_Audio_blinkOperaFix ) delete X_Audio_codecs[ 'mp3' ];
49                 })( X_Audio_codecs );
50         } else {
51                 // iOS3.2.3
52                 X_Audio_codecs = {
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+)
60                 };
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;
67                                 } else {
68                                         delete X_Audio_codecs[ k ];
69                                 };
70                         };
71                 })( X_Audio_codecs );
72         };
73         
74         if( X_Audio_blinkOperaFix ){
75                 X_Audio_constructor = null;
76                 delete X_TEMP.rawAudio;
77         };
78 };
79
80
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 が鳴らない問題を見ていくよ
84                                                                 // !X_UA[ 'iOS' ] &&
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 調査用
90                                                                 //!X_UA[ 'Blink' ] &&
91                                                                 // Firefox40.0.5 + Windows8 で音声が途中から鳴らなくなる
92                                                                 // Firefox41.0.1 + Windows8 で音声が途中から鳴らなくなる
93                                                                 !( 40 <= X_UA[ 'Gecko' ] && X_UA[ 'Gecko' ] < 46 && 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,
98         X_WebAudio,
99         X_WebAudio_BufferLoader,
100         X_WebAudio_fpsFix;
101
102 /*
103  * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
104  */
105 if( X_WebAudio_context ){
106         
107         X_WebAudio_context = new X_WebAudio_context;
108         
109         X_WebAudio_BufferLoader = X_EventDispatcher[ 'inherits' ](
110                 'X.WebAudio.BufferLoader',
111                 X_Class.POOL_OBJECT,
112                 {
113                         audioUrl        : '',
114             xhr             : null,
115             onDecodeSuccess : null,
116             onDecodeError   : null,
117             
118             audioBuffer     : null,
119             errorState      : 0,
120             webAudioList    : null,
121             
122                         'Constructor' : function( webAudio, url ){
123                                 this.webAudioList = [ webAudio ];
124                                 this.audioUrl     = url;
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 );
129                         },
130                         
131                         handleEvent : function( e ){
132                                 var i, l;
133                                 
134                                 switch( e.type ){
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' ] } );
138                                                 };
139                                                 return;
140                                         
141                                         case X_EVENT_SUCCESS :
142                                         // TODO 旧api
143                                         // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
144                                         
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 ) );
151                                                 } else
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 ) );
156                                                 };
157                                                 break;
158
159                                         case X_EVENT_COMPLETE :
160                                                 this.errorState = 1;                            
161                                                 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
162                                                 break;
163                                 };
164                                 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
165                                 delete this.xhr;
166                         },
167                         
168                                 _onDecodeSuccess : function( buffer ){
169                                         this.onDecodeSuccess && this._onDecodeComplete();
170                                         
171                         if( !buffer ){
172                                 this.errorState = 2;
173                             this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
174                             return;
175                         };
176                         
177                         console.log( 'WebAudio decode success!' );
178         
179                         this.audioBuffer = buffer;
180
181                                         this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
182
183                         console.log( 'WebAudio decoded!' );
184                                 },
185                                 
186                                 _onDecodeError : function(){
187                                         console.log( 'WebAudio decode error!' );
188                                         this._onDecodeComplete();
189                                         this.errorState = 2;
190                                         this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
191                                 },
192                                 
193                                 _onDecodeComplete : function(){
194                                         X_Closure_correct( this.onDecodeSuccess );
195                                         delete this.onDecodeSuccess;
196                                         X_Closure_correct( this.onDecodeError );
197                                         delete this.onDecodeError;
198                                 },
199                         
200                         unregister : function( webAudio ){
201                                 var list = this.webAudioList,
202                                         i    = list.indexOf( webAudio );
203
204                                 if( 0 < i ){
205                                         list.splice( i, 1 );
206                                         if( !list.length ){
207                                                 this.xhr && this.xhr[ 'kill' ]();
208                                                 this[ 'kill' ]();
209                                         };
210                                 };
211                         }
212                         
213                 }
214         );
215         
216         
217         X_WebAudio = X_AudioBase[ 'inherits' ](
218                 'X.WebAudio',
219                 X_Class.POOL_OBJECT,
220                 {
221                         
222                         loader          : null,
223                                                 
224                         _startPos       : 0,
225                         _endPosition    : 0,
226                         _startTime      : 0,
227             //_timerID        : 0,
228             _interval       : 0,
229                 audioBuffer     : null,
230                 bufferSource    : null,
231             gainNode        : null,
232             //_onended        : null,
233             
234                         'Constructor' : function( disatcher, url, option ){                             
235                                 var i = 0,
236                                         l = X_WebAudio_BUFFER_LIST.length,
237                                         loader;
238
239                                 /*
240                                  * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
241                                  * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
242                                  * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
243                                  */
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;
247                                 };
248
249                                 for( ; i < l; ++i ){
250                                         loader = X_WebAudio_BUFFER_LIST[ i ];
251                                         if( loader.audioUrl === url ){
252                                                 this.loader = loader;
253                                                 loader.webAudioList.push( this );
254                                                 break;
255                                         };
256                                 };
257                                 
258                                 if( !this.loader ){
259                                         this.loader = loader = X_WebAudio_BufferLoader( this, url );
260                                 };
261                                 
262                                 this.disatcher = disatcher || this;
263                                 this.setState( option );
264                                 
265                                 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
266                                 
267                                 if( loader.audioBuffer || loader.errorState ){
268                                         this._onLoadBufferComplete();
269                                 } else {
270                                         loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
271                                 };
272                         },
273                         
274                         onKill : function(){
275                                 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
276                                         .unregister( this );
277
278                                 delete this.audioBuffer;
279                                 
280                                 this.playing      && this.actualPause();
281                     this.bufferSource && this._sourceDispose();
282         
283                     //this._onended     && X_Closure_correct( this._onended );  
284         
285                     this.gainNode     && this.gainNode.disconnect();
286                         },
287                                 _onLoadBufferComplete : function( e ){
288                                         var loader = this.loader,
289                                                 buffer = loader.audioBuffer;
290                                         
291                                         e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
292                                         
293                         if ( !buffer ) {
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'
301                                                         });
302                                                 this[ 'kill' ]();
303                             return;
304                         };
305         
306                         this.audioBuffer = buffer;
307                         this.duration    = buffer.duration * 1000;
308
309                                         this.disatcher[ 'asyncDispatch' ]( X_WebAudio_touchState ? X_EVENT_MEDIA_TOUCH_FOR_LOAD : X_EVENT_READY );
310                                 },
311                         
312                         actualPlay : function(){
313                                 var e, begin, end;
314                                 
315                                 console.log( '[WebAudio] play abuf:' + !!this.audioBuffer );
316                                 
317                     if( !this.audioBuffer ){
318                         this._playReserved = true;
319                         return;
320                     };
321                                 
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 : '' ) );
326                                                 return;
327                                         };
328                                         // http://qiita.com/uupaa/items/e5856e3cb2a9fc8c5507
329                                         // iOS9 + touchstart で呼んでいた場合、 X_ViewPort['listenOnce']('pointerup',this,this.actualPlay())
330                                         this.disatcher[ 'asyncDispatch' ]( X_EVENT_READY );
331                                 };
332                                 X_WebAudio_touchState = false;
333                                 
334                                 end   = X_Audio_getEndTime( this );
335                                 begin = X_Audio_getStartTime( this, end, true );
336                                 
337                                 console.log( '[WebAudio] play ' + begin + ' -> ' + end + ' loop: ' + this.autoLoop + ' :' + this.loopStartTime + ' -> ' + this.loopEndTime );
338                                 
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' ] );
343                                 };
344                                 
345                     this.bufferSource        = X_WebAudio_context[ 'createBufferSource' ]();
346                     this.bufferSource.buffer = this.audioBuffer;
347                     
348                     /* win8.1 Firefox45, win8.1 Chrome48 で動かなくなる...
349                     if( this.bufferSource[ 'loop' ] = this.autoLoop ){
350                         this.bufferSource[ 'loopStart' ] = 0 <= this.loopStartTime ? this.loopStartTime / 1000 : begin / 1000;
351                         this.bufferSource[ 'loopEnd'   ] = 0 <= this.loopEndTime   ? this.loopEndTime   / 1000 : end   / 1000;
352                     }; */
353                     
354                     this.bufferSource[ 'connect' ]( this.gainNode );
355                     this.gainNode[ 'gain' ].value = this.gain;
356                     
357                     // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
358                     // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
359                     // 多くのブラウザで onended は timer を使ったカウントより遅いので使わない
360                 //if( this.bufferSource.onended !== undefined ){
361                         //console.log( '> use onended' );
362                         //this.bufferSource.onended = this._onended || ( this._onended = X_Closure_create( this, this._onEnded ) );
363                 //} else {
364                         //this._timerID && X_Timer_remove( this._timerID );
365                                         //this._timerID = X_Timer_once( end - begin, this, this._onEnded );
366                 //};
367         
368                     if( this.bufferSource.start ){
369                         this.bufferSource.start( 0, begin / 1000, ( end - begin ) / 1000 );
370                     } else
371                     if( this.bufferSource[ 'noteOn' ] ){
372                         this.bufferSource[ 'noteOn' ]( 0, begin / 1000, ( end - begin ) / 1000 );
373                     } else {
374                         this.bufferSource[ 'noteGrainOn' ]( 0, begin / 1000, ( end - begin ) / 1000 );
375                     };
376                     
377                     this.playing      = true;
378                     this._startPos    = begin;
379                     this._endPosition = end;
380                     this._startTime   = X_WebAudio_context.currentTime * 1000;
381                     this._interval    = this._interval || X_Timer_add( 100, 0, this, this._onInterval );
382                         },
383                         
384                                 _sourceDispose : function(){
385                             this.bufferSource.disconnect();
386                             //delete this.bufferSource.onended;
387                             delete this.bufferSource;
388                         },
389
390                                 /* 
391                                 _onInterval : function(){
392                                         if( !this.playing ){
393                                                 delete this._interval;
394                                                 return X_CALLBACK_UN_LISTEN;
395                                         };
396                                         this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
397                                 }, */
398                                         
399                                 _onInterval : function(){
400                                         var time;
401                                         
402                             if( this.playing ){
403                                 // TODO 再生中に終了時間だけ変えた場合!
404                                 time = X_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
405                                 //console.log( '> onEnd ' + ( this.playing && ( X_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
406
407                                 if( time < 0 ){
408                                         this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
409                                         return;
410                                 };
411                                 
412                                 if( this.autoLoop ){
413                                         if( !( this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
414                                                 this.looped = true;
415                                                 this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
416                                                 this.actualPlay();
417                                         } else {
418                                                                 delete this._interval;
419                                                                 return X_CALLBACK_UN_LISTEN;
420                                         };
421                                 } else {
422                                         this.actualPause();
423                                         this.disatcher[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
424                                 };
425                             };
426                                 },
427                         
428                         actualPause : function(){
429                                 console.log( '[WebAudio] pause' );
430                                 
431                                 this._interval && X_Timer_remove( this._interval );
432                                 delete this._interval;
433                                 delete this.playing;
434
435                     if( this.bufferSource ){
436                         this.bufferSource.stop ? 
437                                 this.bufferSource.stop( 0 ) : this.bufferSource[ 'noteOff' ]( 0 );
438                     };
439                         },
440                         
441                         getActualCurrentTime : function(){
442                                 return X_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
443                         },
444                         
445                         afterUpdateState : function( result ){
446                                 if( result & 2 || result & 1 ){ // seek
447                         this.actualPlay();
448                                 } else
449                                 if( result & 4 ){
450                        this.gainNode[ 'gain' ].value = this.gain;
451                                 };
452                         }
453
454                 }
455         );
456
457         X_Audio_BACKENDS.push(
458                 {
459                         backendID   : 1,
460                         
461                         backendName : 'WebAudio',
462
463                         canPlay     : X_Audio_codecs,
464
465                         detect      : function( proxy, source, ext ){
466                                 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
467                         },
468                         
469                         klass : X_WebAudio
470                 }
471         );
472 };