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,
14 * iPhone 4s 以下、iPad2以下、iPad mini 1以下, iPod touch 4G 以下は不可
16 if( X_Audio_WebAudio_context ){
18 X_Audio_WebAudio_context = new X_Audio_WebAudio_context;
20 X_Audio_BufferLoader = X_EventDispatcher[ 'inherits' ](
21 'X.WebAudio.BufferLoader',
26 onDecodeSuccess : null,
33 'Constructor' : function( webAudio, url ){
34 this.webAudioList = [ webAudio ];
36 this.xhr = X[ 'Net' ]( { 'xhr' : url, 'dataType' : 'arraybuffer' } )
37 [ 'listen' ]( X_EVENT_PROGRESS, this )
38 [ 'listenOnce' ]( [ X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
41 handleEvent : function( e ){
43 case X_EVENT_PROGRESS :
44 this[ 'dispatch' ]( { type : 'progress', 'percent' : e[ 'percent' ] } );
47 case X_EVENT_SUCCESS :
49 // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
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 ) );
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 ) );
65 case X_EVENT_COMPLETE :
67 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
70 this.xhr[ 'unlisten' ]( [ X_EVENT_PROGRESS, X_EVENT_SUCCESS, X_EVENT_COMPLETE ], this );
74 _onDecodeSuccess : function( buffer ){
75 console.log( 'WebAudio decode success!' );
77 this.onDecodeSuccess && this._onDecodeComplete();
81 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
87 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
89 console.log( 'WebAudio decoded!' );
92 _onDecodeError : function(){
93 console.log( 'WebAudio decode error!' );
94 this._onDecodeComplete();
96 this[ 'asyncDispatch' ]( X_EVENT_COMPLETE );
99 _onDecodeComplete : function(){
100 X_Closure_correct( this.onDecodeSuccess );
101 delete this.onDecodeSuccess;
102 X_Closure_correct( this.onDecodeError );
103 delete this.onDecodeError;
106 unregister : function( webAudio ){
107 var list = this.webAudioList,
108 i = list.indexOf( webAudio );
112 this.xhr && this.xhr[ 'kill' ]();
122 X_Audio_WebAudioWrapper = X_Audio_AbstractAudioBackend[ 'inherits' ](
139 'Constructor' : function( target, url, option ){
141 l = X_Audio_BUFFER_LIST.length,
145 * http://qiita.com/sou/items/5688d4e7d3a37b4e2ff1
146 * L-01F 等の一部端末で Web Audio API の再生結果に特定条件下でノイズが混ざることがある。
147 * 描画レート(描画 FPS)が下がるとノイズが混ざり始め、レートを上げると再生結果が正常になるというもので、オーディオ処理が描画スレッドに巻き込まれているような動作を見せる。
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;
155 loader = X_Audio_BUFFER_LIST[ i ];
156 if( loader.url === url ){
157 this.loader = loader;
158 loader.webAudioList.push( this );
164 this.loader = loader = new X_Audio_BufferLoader( this, url );
167 this.target = target || this;
169 this.setState( option );
171 this[ 'listenOnce' ]( X_EVENT_KILL_INSTANCE, this.onKill );
173 if( loader.buffer || loader.error ){
174 this._onLoadBufferComplete();
176 loader[ 'listenOnce' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
181 this.loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete )
186 this.playing && this.actualPause();
187 this.bufferSource && this._sourceDispose();
189 this._onended && X_Closure_correct( this._onended );
191 this.gainNode && this.gainNode.disconnect();
193 _onLoadBufferComplete : function( e ){
194 var loader = this.loader,
195 buffer = loader.buffer;
197 e && loader[ 'unlisten' ]( X_EVENT_COMPLETE, this, this._onLoadBufferComplete );
200 this.error = loader.error;
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'
213 this.buffer = buffer;
214 this.duration = buffer.duration * 1000;
216 this.target[ 'asyncDispatch' ]( X_EVENT_READY );
218 console.log( 'WebAudio buffer ready' );
220 this.autoplay && X_Timer_once( 16, this, this.play );
224 actualPlay : function(){
228 this.autoplay = true;
232 end = X_AudioWrapper_getEndTime( this );
233 begin = X_AudioWrapper_getStartTime( this, end, true );
235 console.log( '[WebAudio] play ' + begin + ' -> ' + end );
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' ] );
242 this.bufferSource = X_Audio_WebAudio_context[ 'createBufferSource' ]();
243 this.bufferSource.buffer = this.buffer;
244 this.bufferSource[ 'connect' ]( this.gainNode );
246 this.gainNode[ 'gain' ].value = this.gain;
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 ) );
254 this._timerID && X_Timer_remove( this._timerID );
255 this._timerID = X_Timer_once( end - begin, this, this._onEnded );
258 if( this.bufferSource.start ){
259 this.bufferSource.start( 0, begin / 1000, end / 1000 );
261 this.bufferSource[ 'noteGrainOn' ]( 0, begin / 1000, end / 1000 );
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 );
271 _sourceDispose : function(){
272 this.bufferSource.disconnect();
273 delete this.bufferSource.onended;
274 delete this.bufferSource;
277 _onInterval : function(){
279 delete this._interval;
280 return X_CALLBACK_UN_LISTEN;
282 this.target[ 'dispatch' ]( X_EVENT_MEDIA_PLAYING );
285 _onEnded : function(){
287 delete this._timerID;
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 ) );
294 if( time < 0 ) return;
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 );
305 if( !( this.target[ 'dispatch' ]( X_EVENT_MEDIA_BEFORE_LOOP ) & X_CALLBACK_PREVENT_DEFAULT ) ){
307 this.target[ 'dispatch' ]( X_EVENT_MEDIA_LOOPED );
312 this.target[ 'dispatch' ]( X_EVENT_MEDIA_ENDED );
317 actualPause : function(){
318 //if( !this.playing ) return this;
320 console.log( '[WebAudio] pause' );
322 this.seekTime = this.getActualCurrentTime();
324 this._timerID && X_Timer_remove( this._timerID );
325 delete this._timerID;
328 if( this.bufferSource ){
329 if( this.bufferSource.onended ) delete this.bufferSource.onended;
331 this.bufferSource.stop ?
332 this.bufferSource.stop( 0 ) : this.bufferSource[ 'noteOff' ]( 0 );
336 getActualCurrentTime : function(){
337 return X_Audio_WebAudio_context.currentTime * 1000 - this._startTime + this._startPos | 0;
340 afterUpdateState : function( result ){
341 if( result & 2 || result & 1 ){ // seek
345 this.gainNode[ 'gain' ].value = this.gain;
352 X_Audio_BACKENDS.push(
354 backendName : 'Web Audio',
356 canPlay : {}, // TODO HTMLAudio と同じ
359 detect : function( proxy, source, ext ){
360 proxy[ 'asyncDispatch' ]( { type : X_EVENT_COMPLETE, canPlay : X_Audio_codecs[ ext ] } );
363 klass : X_Audio_WebAudioWrapper