OSDN Git Service

Version 0.6.176, add X.Script.
[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 = !X_UA[ 'iPhone_4s' ]  && !X_UA[ 'iPad_2Mini1' ]  && !X_UA[ 'iPod_4' ]  &&
82                                                                 // TODO なんで fennec を禁止?
83                                                                 !( X_UA[ 'Gecko' ] && X_UA[ 'Android' ] ) &&
84                                                                 // Firefox40.0.5 + Windows8 で音声が途中から鳴らなくなる
85                                                                 // Firefox41.0.1 + Windows8 で音声が途中から鳴らなくなる
86                                                                 !( 40 <= X_UA[ 'Gecko' ] && X_UA[ 'Gecko' ] < 42 && X_UA[ 'Windows' ] ) &&
87                                                                 ( window[ 'AudioContext' ] || window[ 'webkitAudioContext' ] ),
88         X_WebAudio_BUFFER_LIST      = [],
89         X_WebAudio,
90         X_WebAudio_BufferLoader,
91         X_WebAudio_fpsFix;
92
93 /*
94  * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
95  */
96 if( X_WebAudio_context ){
97         
98         X_WebAudio_context = new X_WebAudio_context;
99         
100         X_WebAudio_BufferLoader = X_EventDispatcher[ 'inherits' ](
101                 'X.WebAudio.BufferLoader',
102                 X_Class.POOL_OBJECT,
103                 {
104                         audioUrl        : '',
105             xhr             : null,
106             onDecodeSuccess : null,
107             onDecodeError   : null,
108             
109             audioBuffer     : null,
110             errorState      : 0,
111             webAudioList    : null,
112             
113                         'Constructor' : function( webAudio, url ){
114                                 this.webAudioList = [ webAudio ];
115                                 this.audioUrl     = url;
116                                 this.xhr = X[ 'Net' ]( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
117                                                                         [ 'listen' ]( X_EVENT_PROGRESS, this )
118                                                                         [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
119                                 X_WebAudio_BUFFER_LIST.push( this );
120                         },
121                         
122                         handleEvent : function( e ){
123                                 switch( e.type ){
124                                         case X_EVENT_PROGRESS :
125                                                 this[ 'dispatch' ]( { type : 'progress', 'percent' : e[ 'percent' ] } );
126                                                 return;
127                                         
128                                         case X_EVENT_SUCCESS :
129                                         // TODO 旧api
130                                         // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
131                                         
132                                         // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
133                                         // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
134                                         // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
135                                         // ただし iOS 8.1.2 では エラーになる
136                                                 if( X_UA[ 'iOS' ] < 8 || !X_WebAudio_context[ 'decodeAudioData' ] ){
137                                                         this._onDecodeSuccess( X_WebAudio_context[ 'createBuffer' ]( e.response, false ) );
138                                                 } else
139                                                 if( X_WebAudio_context[ 'decodeAudioData' ] ){
140                                                         X_WebAudio_context[ 'decodeAudioData' ]( e.response,
141                                                                 this.onDecodeSuccess = X_Closure_create( this, this._onDecodeSuccess ),
142                                                                 this.onDecodeError   = X_Closure_create( this, this._onDecodeError ) );
143                                                 };
144                                                 break;
145
146                                         case X_EVENT_COMPLETE :
147                                                 this.errorState = 1;                            
148                                                 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
149                                                 break;
150                                 };
151                                 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
152                                 delete this.xhr;
153                         },
154                         
155                                 _onDecodeSuccess : function( buffer ){
156                                         this.onDecodeSuccess && this._onDecodeComplete();
157                                         
158                         if ( !buffer ) {
159                                 this.errorState = 2;
160                             this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
161                             return;
162                         };
163                         
164                         console.log( 'WebAudio decode success!' );
165         
166                         this.audioBuffer = buffer;
167
168                                         this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
169
170                         console.log( 'WebAudio decoded!' );
171                                 },
172                                 
173                                 _onDecodeError : function(){
174                                         console.log( 'WebAudio decode error!' );
175                                         this._onDecodeComplete();
176                                         this.errorState = 2;
177                                         this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
178                                 },
179                                 
180                                 _onDecodeComplete : function(){
181                                         X_Closure_correct( this.onDecodeSuccess );
182                                         delete this.onDecodeSuccess;
183                                         X_Closure_correct( this.onDecodeError );
184                                         delete this.onDecodeError;
185                                 },
186                         
187                         unregister : function( webAudio ){
188                                 var list = this.webAudioList,
189                                         i    = list.indexOf( webAudio );
190                                 if( 0 < i ){
191                                         list.splice( i, 1 );
192                                         if( list.length ){
193                                                 this.xhr && this.xhr[ 'kill' ]();
194                                                 this[ 'kill' ]();
195                                         };
196                                 };
197                         }
198                         
199                 }
200         );
201         
202         
203         X_WebAudio = X_AudioBase[ 'inherits' ](
204                 'X.WebAudio',
205                 X_Class.POOL_OBJECT,
206                 {
207                         
208                         loader          : null,
209                                                 
210                         _startPos       : 0,
211                         _endPosition    : 0,
212                         _startTime      : 0,
213             _timerID        : 0,
214             _interval       : 0,
215                 audioBuffer     : null,
216                 bufferSource    : null,
217             gainNode        : null,
218             _onended        : null,
219             
220                         'Constructor' : function( target, url, option ){                                
221                                 var i = 0,
222                                         l = X_WebAudio_BUFFER_LIST.length,
223                                         loader;
224
225                                 /*
226                                  * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
227                                  * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
228                                  * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
229                                  */
230                                 if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] && !X_WebAudio_fpsFix ){
231                                         X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } );
232                                         X_WebAudio_fpsFix = true;
233                                 };
234
235                                 for( ; i < l; ++i ){
236                                         loader = X_WebAudio_BUFFER_LIST[ i ];
237                                         if( loader.audioUrl === url ){
238                                                 this.loader = loader;
239                                                 loader.webAudioList.push( this );
240                                                 break;
241                                         };
242                                 };
243                                 
244                                 if( !this.loader ){
245                                         this.loader = loader = X_WebAudio_BufferLoader( this, url );
246                                 };
247                                 
248                                 this.target  = target || this;
249                                 
250                                 this.setState( option );
251                                 
252                                 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
253                                 
254                                 if( loader.audioBuffer || loader.errorState ){
255                                         this._onLoadBufferComplete();
256                                 } else {
257                                         loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
258                                 };
259                         },
260                         
261                         onKill : function(){
262                                 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
263                                         .unregister( this );
264
265                                 delete this.audioBuffer;
266                                 
267                                 this.playing      && this.actualPause();
268                     this.bufferSource && this._sourceDispose();
269         
270                     this._onended     && X_Closure_correct( this._onended );    
271         
272                     this.gainNode     && this.gainNode.disconnect();
273                         },
274                                 _onLoadBufferComplete : function( e ){
275                                         var loader = this.loader,
276                                                 buffer = loader.audioBuffer;
277                                         
278                                         e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
279                                         
280                         if ( !buffer ) {
281                                 this.error = loader.errorState;
282                                 
283                             this.target[ 'dispatch' ]({
284                                                                 type    : X_EVENT_ERROR,
285                                                                 error   : loader.errorState,
286                                                                 message : loader.errorState === 1 ?
287                                                                                         'load buffer network error' :
288                                                                                         'buffer decode error'
289                                                         });
290                                                 this[ 'kill' ]();
291                             return;
292                         };
293         
294                         this.audioBuffer = buffer;
295                         this.duration    = buffer.duration * 1000;
296
297                                         this.target[ 'asyncDispatch' ]( X_EVENT_READY );
298                         
299                         console.log( 'WebAudio buffer ready' );
300                         
301                         this.autoplay && X_Timer_once( 16, this, this.play );
302                                         
303                                 },
304                         
305                         actualPlay : function(){
306                                 var begin, end;
307                                 
308                     if( !this.audioBuffer ){
309                         this.autoplay = true;
310                         return;
311                     };
312                                 
313                                 end   = X_Audio_getEndTime( this );
314                                 begin = X_Audio_getStartTime( this, end, true );
315                                 
316                                 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
317                                 
318                                 if( this.bufferSource ) this._sourceDispose();
319                                 if( !this.gainNode ){
320                                         this.gainNode = X_WebAudio_context[ 'createGain' ] ? X_WebAudio_context[ 'createGain' ]() : X_WebAudio_context[ 'createGainNode' ]();
321                         this.gainNode[ 'connect' ]( X_WebAudio_context[ 'destination' ] );
322                                 };
323                     this.bufferSource        = X_WebAudio_context[ 'createBufferSource' ]();
324                     this.bufferSource.buffer = this.audioBuffer;
325                     this.bufferSource[ 'connect' ]( this.gainNode );
326                     
327                     this.gainNode[ 'gain' ].value = this.gain;
328                     
329                     // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
330                     // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
331                     // 多くのブラウザで onended は timer を使ったカウントより遅いので使わない
332                 //if( this.bufferSource.onended !== undefined ){
333                         //console.log( '> use onended' );
334                         //this.bufferSource.onended = this._onended || ( this._onended = X_Closure_create( this, this._onEnded ) );
335                 //} else {
336                         this._timerID && X_Timer_remove( this._timerID );
337                                         this._timerID = X_Timer_once( end - begin, this, this._onEnded );
338                 //};
339         
340                     if( this.bufferSource.start ){
341                         this.bufferSource.start( 0, begin / 1000, end / 1000 );
342                     } else {
343                         this.bufferSource[ 'noteGrainOn' ]( 0, begin / 1000, end / 1000 );
344                     };
345                     
346                     this.playing      = true;
347                     this._startPos    = begin;
348                     this._endPosition = end;
349                     this._startTime   = X_WebAudio_context.currentTime * 1000;
350                     this._interval    = this._interval || X_Timer_add( 1000, 0, this, this._onInterval );
351                         },
352                         
353                                 _sourceDispose : function(){
354                             this.bufferSource.disconnect();
355                             delete this.bufferSource.onended;
356                             delete this.bufferSource;
357                         },
358
359                                 _onInterval : function(){
360                                         if( !this.playing ){
361                                                 delete this._interval;
362                                                 return X_CALLBACK_UN_LISTEN;
363                                         };
364                                         this.target[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
365                                 },
366                                                 
367                                 _onEnded : function(){
368                                         var time;
369                                         delete this._timerID;
370                                         
371                             if( this.playing ){
372                                 time = X_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
373                                 //console.log( '> onEnd ' + ( this.playing && ( X_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
374                                 if( this._onended ){
375                                         // Firefox 用の対策,,,
376                                         if( time < 0 ) return;
377                                 } else {
378                                         if( time < 0 ){
379                                                 //console.log( '> onEnd crt:' + ( X_WebAudio_context.currentTime * 1000 ) + ' startTime:' + this._startTime +
380                                                 //      ' from:' + this._startPos + ' to:' + this._endPosition );
381                                                 this._timerID = X_Timer_once( -time, this, this._onEnded );
382                                                 return;
383                                         };
384                                 };
385                                 
386                                 if( this.autoLoop ){
387                                         if( !( this.target[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
388                                                 this.looped = true;
389                                                 this.target[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
390                                                 this.actualPlay();
391                                         };
392                                 } else {
393                                         this.actualPause();
394                                         this.target[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
395                                 };
396                             };
397                                 },
398                         
399                         actualPause : function(){
400                                 //if( !this.playing ) return this;
401                                 
402                                 console.log( '[WebAudio] pause' );
403                                 
404                                 this.seekTime = this.getActualCurrentTime();
405                                 
406                     this._timerID && X_Timer_remove( this._timerID );
407                                 delete this._timerID;
408                                 delete this.playing;
409
410                     if( this.bufferSource ){
411                         if( this.bufferSource.onended ) delete this.bufferSource.onended;
412                         
413                         this.bufferSource.stop ? 
414                                 this.bufferSource.stop( 0 ) : this.bufferSource[ 'noteOff' ]( 0 );
415                     };
416                         },
417                         
418                         getActualCurrentTime : function(){
419                                 return X_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
420                         },
421                         
422                         afterUpdateState : function( result ){
423                                 if( result & 2 || result & 1 ){ // seek
424                         this.actualPlay();
425                                 } else
426                                 if( result & 4 ){
427                        this.gainNode[ 'gain' ].value = this.gain;
428                                 };
429                         }
430
431                 }
432         );
433
434         X_Audio_BACKENDS.push(
435                 {
436                         backendID   : 1,
437                         
438                         backendName : 'WebAudio',
439
440                         canPlay     : X_Audio_codecs,
441
442                         detect      : function( proxy, source, ext ){
443                                 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
444                         },
445                         
446                         klass : X_WebAudio
447                 }
448         );
449 };