OSDN Git Service

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