OSDN Git Service

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