OSDN Git Service

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