OSDN Git Service

Version 0.6.172, bugfix...
[pettanr/clientJs.git] / 0.6.x / js / 07_audio / 01_XWebAudio.js
1
2 var X_Audio_WebAudio_context = !X_UA[ 'iPhone_4s' ]  && !X_UA[ 'iPad_2Mini1' ]  && !X_UA[ 'iPod_4' ]  &&
3                                                                 // TODO なんで fennec を禁止?
4                                                                 !( X_UA[ 'Gecko' ] && X_UA[ 'Android' ] ) &&
5                                                                 // Firefox40.0.5 + Windows8 で音声が鳴らない
6                                                                 !( X_UA[ 'Gecko' ] === 40 && X_UA[ 'Windows' ] ) &&
7                                                                 ( window[ 'AudioContext' ] || window[ 'webkitAudioContext' ] ),
8         X_Audio_BUFFER_LIST      = [],
9         X_Audio_WebAudioWrapper,
10         X_Audio_BufferLoader,
11         X_Audio_fpsFix;
12
13 /*
14  * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
15  */
16 if( X_Audio_WebAudio_context ){
17         
18         X_Audio_WebAudio_context = new X_Audio_WebAudio_context;
19         
20         X_Audio_BufferLoader = X_EventDispatcher[ 'inherits' ](
21                 'X.WebAudio.BufferLoader',
22                 X_Class.POOL_OBJECT,
23                 {
24                         url             : '',
25             xhr             : null,
26             onDecodeSuccess : null,
27             onDecodeError   : null,
28             
29             buffer          : null,
30             error           : 0,
31             webAudioList    : null,
32             
33                         'Constructor' : function( webAudio, url ){
34                                 this.webAudioList = [ webAudio ];
35                                 this.url = url;
36                                 this.xhr = X[ 'Net' ]( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
37                                                                         [ 'listen' ]( X_EVENT_PROGRESS, this )
38                                                                         [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
39                         },
40                         
41                         handleEvent : function( e ){
42                                 switch( e.type ){
43                                         case X_EVENT_PROGRESS :
44                                                 this[ 'dispatch' ]( { type : 'progress', 'percent' : e[ 'percent' ] } );
45                                                 return;
46                                         
47                                         case X_EVENT_SUCCESS :
48                                         // TODO 旧api
49                                         // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
50                                         
51                                         // http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
52                                         // iOS 7.1 で decodeAudioData に処理が入った瞬間にスクリーンを長押しする(スクロールを繰り返す)と
53                                         // decoeAudioData の処理がキャンセルされることがある(エラーやコールバックの発火もなく、ただ処理が消滅する)。
54                                         // ただし iOS 8.1.2 では エラーになる
55                                                 if( X_UA[ 'iOS' ] < 8 || !X_Audio_WebAudio_context[ 'decodeAudioData' ] ){
56                                                         this._onDecodeSuccess( X_Audio_WebAudio_context[ 'createBuffer' ]( e.response, false ) );
57                                                 } else
58                                                 if( X_Audio_WebAudio_context[ 'decodeAudioData' ] ){
59                                                         X_Audio_WebAudio_context[ 'decodeAudioData' ]( e.response,
60                                                                 this.onDecodeSuccess = X_Closure_create( this, this._onDecodeSuccess ),
61                                                                 this.onDecodeError   = X_Closure_create( this, this._onDecodeError ) );
62                                                 };
63                                                 break;
64
65                                         case X_EVENT_COMPLETE :
66                                                 this.error = 1;                         
67                                                 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
68                                                 break;
69                                 };
70                                 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
71                                 delete this.xhr;
72                         },
73                         
74                                 _onDecodeSuccess : function( buffer ){
75                                         console.log( 'WebAudio decode success!' );
76                                         
77                                         this.onDecodeSuccess && this._onDecodeComplete();
78                                         
79                         if ( !buffer ) {
80                                 this.error = 2;
81                             this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
82                             return;
83                         };
84         
85                         this.buffer   = buffer;
86
87                                         this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
88
89                         console.log( 'WebAudio decoded!' );
90                                 },
91                                 
92                                 _onDecodeError : function(){
93                                         console.log( 'WebAudio decode error!' );
94                                         this._onDecodeComplete();
95                                         this.error = 2;
96                                         this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
97                                 },
98                                 
99                                 _onDecodeComplete : function(){
100                                         X_Closure_correct( this.onDecodeSuccess );
101                                         delete this.onDecodeSuccess;
102                                         X_Closure_correct( this.onDecodeError );
103                                         delete this.onDecodeError;
104                                 },
105                         
106                         unregister : function( webAudio ){
107                                 var list = this.webAudioList,
108                                         i    = list.indexOf( webAudio );
109                                 if( 0 < i ){
110                                         list.splice( i, 1 );
111                                         if( list.length ){
112                                                 this.xhr && this.xhr[ 'kill' ]();
113                                                 this[ 'kill' ]();
114                                         };
115                                 };
116                         }
117                         
118                 }
119         );
120         
121         
122         X_Audio_WebAudioWrapper = X_Audio_AbstractAudioBackend[ 'inherits' ](
123                 'X.WebAudio',
124                 X_Class.POOL_OBJECT,
125                 {
126                         
127                         loader          : null,
128                                                 
129                         _startPos       : 0,
130                         _endPosition    : 0,
131                         _startTime      : 0,
132             _timerID        : 0,
133             _interval       : 0,
134                 buffer          : null,
135                 bufferSource    : null,
136             gainNode        : null,
137             _onended        : null,
138             
139                         'Constructor' : function( target, url, option ){                                
140                                 var i = 0,
141                                         l = X_Audio_BUFFER_LIST.length,
142                                         loader;
143
144                                 /*
145                                  * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
146                                  * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
147                                  * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
148                                  */
149                                 if( X_UA[ 'Android' ] && X_UA[ 'Chrome' ] && !X_Audio_fpsFix ){
150                                         X_Node_systemNode.create( 'div', { id : 'fps-slowdown-make-sound-noisy' } );
151                                         X_Audio_fpsFix = true;
152                                 };
153
154                                 for( ; i < l; ++i ){
155                                         loader = X_Audio_BUFFER_LIST[ i ];
156                                         if( loader.url === url ){
157                                                 this.loader = loader;
158                                                 loader.webAudioList.push( this );
159                                                 break;
160                                         };
161                                 };
162                                 
163                                 if( !this.loader ){
164                                         this.loader = loader = new X_Audio_BufferLoader( this, url );
165                                 };
166                                 
167                                 this.target  = target || this;
168                                 
169                                 this.setState( option );
170                                 
171                                 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
172                                 
173                                 if( loader.buffer || loader.error ){
174                                         this._onLoadBufferComplete();
175                                 } else {
176                                         loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
177                                 };
178                         },
179                         
180                         onKill : function(){
181                                 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
182                                         .unregister( this );
183
184                                 delete this.buffer;
185                                 
186                                 this.playing      && this.actualPause();
187                     this.bufferSource && this._sourceDispose();
188         
189                     this._onended     && X_Closure_correct( this._onended );    
190         
191                     this.gainNode     && this.gainNode.disconnect();
192                         },
193                                 _onLoadBufferComplete : function( e ){
194                                         var loader = this.loader,
195                                                 buffer = loader.buffer;
196                                         
197                                         e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
198                                         
199                         if ( !buffer ) {
200                                 this.error = loader.error;
201                                 
202                             this.target[ 'dispatch' ]({
203                                                                 type    : X_EVENT_ERROR,
204                                                                 error   : loader.error,
205                                                                 message : loader.error === 1 ?
206                                                                                         'load buffer network error' :
207                                                                                         'buffer decode error'
208                                                         });
209                                                 this[ 'kill' ]();
210                             return;
211                         };
212         
213                         this.buffer   = buffer;
214                         this.duration = buffer.duration * 1000;
215
216                                         this.target[ 'asyncDispatch' ]( X_EVENT_READY );
217                         
218                         console.log( 'WebAudio buffer ready' );
219                         
220                         this.autoplay && X_Timer_once( 16, this, this.play );
221                                         
222                                 },
223                         
224                         actualPlay : function(){
225                                 var begin, end;
226                                 
227                     if( !this.buffer ){
228                         this.autoplay = true;
229                         return;
230                     };
231                                 
232                                 end   = X_AudioWrapper_getEndTime( this );
233                                 begin = X_AudioWrapper_getStartTime( this, end, true );
234                                 
235                                 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
236                                 
237                                 if( this.bufferSource ) this._sourceDispose();
238                                 if( !this.gainNode ){
239                                         this.gainNode = X_Audio_WebAudio_context[ 'createGain' ] ? X_Audio_WebAudio_context[ 'createGain' ]() : X_Audio_WebAudio_context[ 'createGainNode' ]();
240                         this.gainNode[ 'connect' ]( X_Audio_WebAudio_context[ 'destination' ] );
241                                 };
242                     this.bufferSource        = X_Audio_WebAudio_context[ 'createBufferSource' ]();
243                     this.bufferSource.buffer = this.buffer;
244                     this.bufferSource[ 'connect' ]( this.gainNode );
245                     
246                     this.gainNode[ 'gain' ].value = this.gain;
247                     
248                     // おかしい、stop 前に外していても呼ばれる、、、@Firefox33.1
249                     // 破棄された X.Callback が呼ばれて、obj.proxy() でエラーになる。Firefox では、onended は使わない
250                 if( false && this.bufferSource.onended !== undefined ){
251                         //console.log( '> use onended' );
252                         this.bufferSource.onended = this._onended || ( this._onended = X_Closure_create( this, this._onEnded ) );
253                 } else {
254                         this._timerID && X_Timer_remove( this._timerID );
255                                         this._timerID = X_Timer_once( end - begin, this, this._onEnded );
256                 };
257         
258                     if( this.bufferSource.start ){
259                         this.bufferSource.start( 0, begin / 1000, end / 1000 );
260                     } else {
261                         this.bufferSource[ 'noteGrainOn' ]( 0, begin / 1000, end / 1000 );
262                     };
263                     
264                     this.playing      = true;
265                     this._startPos    = begin;
266                     this._endPosition = end;
267                     this._startTime   = X_Audio_WebAudio_context.currentTime * 1000;
268                     this._interval    = this._interval || X_Timer_add( 1000, 0, this, this._onInterval );
269                         },
270                         
271                                 _sourceDispose : function(){
272                             this.bufferSource.disconnect();
273                             delete this.bufferSource.onended;
274                             delete this.bufferSource;
275                         },
276
277                                 _onInterval : function(){
278                                         if( !this.playing ){
279                                                 delete this._interval;
280                                                 return X_CALLBACK_UN_LISTEN;
281                                         };
282                                         this.target[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
283                                 },
284                                                 
285                                 _onEnded : function(){
286                                         var time;
287                                         delete this._timerID;
288                                         
289                             if( this.playing ){
290                                 time = X_Audio_WebAudio_context.currentTime * 1000 - this._startTime - this._endPosition + this._startPos | 0;
291                                 //console.log( '> onEnd ' + ( this.playing && ( X_Audio_WebAudio_context.currentTime * 1000 - this._startTime ) ) + ' < ' + ( this._endPosition - this._startPos ) );
292                                 if( this._onended ){
293                                         // Firefox 用の対策,,,
294                                         if( time < 0 ) return;
295                                 } else {
296                                         if( time < 0 ){
297                                                 //console.log( '> onEnd crt:' + ( X_Audio_WebAudio_context.currentTime * 1000 ) + ' startTime:' + this._startTime +
298                                                 //      ' from:' + this._startPos + ' to:' + this._endPosition );
299                                                 this._timerID = X_Timer_once( -time, this, this._onEnded );
300                                                 return;
301                                         };
302                                 };
303                                 
304                                 if( this.autoLoop ){
305                                         if( !( this.target[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
306                                                 this.looped = true;
307                                                 this.target[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
308                                                 this.actualPlay();
309                                         };
310                                 } else {
311                                         this.actualPause();
312                                         this.target[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
313                                 };
314                             };
315                                 },
316                         
317                         actualPause : function(){
318                                 if( !this.playing ) return this;
319                                 
320                                 console.log( '[WebAudio] pause' );
321                                 
322                                 this.seekTime = this.getActualCurrentTime();
323                                 
324                     this._timerID && X_Timer_remove( this._timerID );
325                                 delete this._timerID;
326                                 delete this.playing;
327
328                     if( this.bufferSource ){
329                         if( this.bufferSource.onended ) delete this.bufferSource.onended;
330                         
331                         this.bufferSource.stop ? 
332                                 this.bufferSource.stop( 0 ) : this.bufferSource[ 'noteOff' ]( 0 );
333                     };
334                         },
335                         
336                         getActualCurrentTime : function(){
337                                 return X_Audio_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
338                         },
339                         
340                         afterUpdateState : function( result ){
341                                 if( result & 2 || result & 1 ){ // seek
342                         this.actualPlay();
343                                 } else
344                                 if( result & 4 ){
345                        this.gainNode[ 'gain' ].value = this.gain;
346                                 };
347                         }
348
349                 }
350         );
351
352         X_Audio_BACKENDS.push(
353                 {
354                         backendName : 'Web Audio',
355
356                         canPlay : {}, // TODO HTMLAudio と同じ
357
358                         // 
359                         detect : function( proxy, source, ext ){
360                                 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
361                         },
362                         
363                         klass : X_Audio_WebAudioWrapper
364                 }
365         );
366 };