OSDN Git Service

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