13 var X_Audio_BACKENDS = []; // Array.<Hash>
\r
15 X_TEMP.onSystemReady.push(
\r
17 var canPlay = X[ 'Audio' ][ 'canPlay' ] = {},
\r
18 i = X_Audio_BACKENDS.length,
\r
21 be = X_Audio_BACKENDS[ --i ];
\r
22 X_Object_override( canPlay, be.canPlay );
\r
23 X[ 'Audio' ][ be.backendName ] = be.backendID;
\r
28 * <p>複数のオーディオ・バックエンドから、与えられた音声を再生可能なものを見つけ、音声を再生します。
\r
29 * <p>HTMLAudio の動作・機能がブラウザ毎にバラバラなのに業を煮やし、メソッドやイベントは独自に定義しています。
\r
30 * <h4>バックエンドの種類</h4>
\r
31 * <p>HTMLAudio, WebAudio, Silverlight, WMP
\r
34 * <dt>X.Event.BACKEND_READY <dd>音声(src リスト)を再生可能なバックエンドが見つかった。
\r
35 * <dt>X.Event.BACKEND_NONE <dd>音声を再生可能なバックエンドが見つからなかった。Audio は kill されます。
\r
36 * <dt>X.Event.MEDIA_CAN_TOUCH <dd>モバイル端末の制約で音声の再生またはロードに、タッチを必要とする場合、タッチイベント内で play を呼び出す準備が出来たことを通知する。
\r
37 * <dt>X.Event.READY <dd>再生可能、実際の状態は canplay から loadeddata まで様々、、、モバイル端末の場合、タッチして再生が開始された場合に
\r
38 * <dt>X.Event.ERROR <dd><ul>
\r
39 * <li> 1 : ユーザーによってメディアの取得が中断された
\r
40 * <li> 2 : ネットワークエラー
\r
41 * <li> 3 : メディアのデコードエラー
\r
42 * <li> 4 : メディアがサポートされていない
\r
44 * <dt>X.Event.MEDIA_PLAYING <dd>再生中に1秒以下のタイミングで発生.currentTime が取れる?
\r
45 * <dt>X.Event.MEDIA_LOOP <dd>ループ直前に発生、キャンセル可能
\r
46 * <dt>X.Event.MEDIA_LOOPED <dd>ループ時に発生
\r
47 * <dt>X.Event.MEDIA_ENDED <dd>再生位置の(音声の)最後についた
\r
48 * <dt>X.Event.MEDIA_PAUSED <dd>ポーズした
\r
49 * <dt>X.Event.MEDIA_WAITING <dd>再生中に音声が待機状態に。
\r
50 * <dt>X.Event.MEDIA_SEEKING <dd>シーク中に音声が待機状態に。
\r
52 * <h4>ソースリストに与える url 文字列</h4>
\r
53 * <p>ハッシュフラグメント以下にデータを書くことで、各オーディオバックエンドが再生可能性の判断にあたって参考にするデータを渡すことができます。
\r
55 * <dt>CBR=1<dd>audio が固定ビットレートであることを示す。Android 用 Opera12- は可変ビットレートの mp3 を正しくシークできない。
\r
56 * [ 'snd.mp3', 'snd.mp3#CBR=1' ] と指定すると、Android 用 Opera12- では CBR な mp3 が、他の環境ではよりファイルサイズの小さい VBR な mp3 が使用される。(未実装)
\r
57 * <dt>ext=mp3<dd>パスに拡張子が含まれない場合、または上書き指定したい場合に指定する
\r
60 * @class 各種オーディオ機能をラップしインターフェイスを共通化する。
\r
62 * @extends {EventDispatcher}
\r
63 * @param {array|string} sourceList
\r
64 * @param {object=} opt_option
\r
66 * var audio = X.Audio( [ 'etc/special.mp3', 'etc/special.ogg', 'etc/special.wav' ] ).listenOnce( X.Event.READY, onReady );
\r
68 X[ 'Audio' ] = X_EventDispatcher[ 'inherits' ](
\r
73 * 音声の url。X.Event.BACKEND_READY で設定される。
\r
74 * @alias Audio.prototype.source
\r
80 * 音声再生バックエンドの名前。X.Event.BACKEND_READY で設定される。
\r
81 * @alias Audio.prototype.backendName
\r
86 'Constructor' : function( sourceList, opt_option ){
\r
87 X_Audio_startDetectionBackend(
\r
88 X_Audio_BACKENDS[ 0 ], this,
\r
89 X_Type_isArray( sourceList ) ? X_Array_copy( sourceList ) : [ sourceList ],
\r
91 this[ 'listenOnce' ]( [ X_EVENT_BACKEND_READY, X_EVENT_BACKEND_NONE, X_EVENT_KILL_INSTANCE ], X_Audio_handleEvent );
\r
92 X_ViewPort[ 'listenOnce' ]( X_EVENT_UNLOAD, this, X_Audio_handleEvent );
\r
96 * 再生。開始位置・終了位置、ループの有無、ループ以降の開始位置、ループ以降の終了位置
\r
97 * @alias Audio.prototype.play
\r
98 * @param {number=} startTime 開始時間を ms で
\r
99 * @param {number=} endTime 終了時間を ms で
\r
100 * @param {boolean=} loop endTimeに達した際に曲をループさせるか
\r
101 * @param {number=} loopStartTime ループ以後の開始時間を ms で
\r
102 * @param {number=} loopEndTime ループ以後の終了時間を ms で
\r
103 * @return {Audio} メソッドチェーン
\r
105 'play' : function( startTime, endTime, loop, loopStartTime, loopEndTime ){
\r
106 var pair = X_Pair_get( this );
\r
107 pair && pair.play( startTime, endTime, loop, loopStartTime, loopEndTime );
\r
111 * シーク、再生中で無い場合は次回再生開始位置の指定のみ
\r
112 * @alias Audio.prototype.seek
\r
113 * @param {number} seekTime シーク位置を ms で
\r
114 * @return {Audio} メソッドチェーン
\r
116 'seek' : function( seekTime ){
\r
117 var pair = X_Pair_get( this );
\r
118 pair && pair.seek( seekTime );
\r
123 * @alias Audio.prototype.pause
\r
124 * @return {Audio} メソッドチェーン
\r
126 'pause' : function(){
\r
127 var pair = X_Pair_get( this );
\r
128 pair && pair.pause();
\r
132 * 状態の getter と setter
\r
133 * @alias Audio.prototype.state
\r
134 * @param {object=} obj setter の場合、上書きする値を格納したobject
\r
135 * @return {Audio|object}
\r
141 'loopStartTime' : 120000,
\r
142 'loopEndTime' : 200000,
\r
150 'state' : function( obj ){
\r
151 var pair = X_Pair_get( this );
\r
152 if( obj === undefined ){
\r
153 return pair ? pair.getState() :
\r
157 'loopStartTime' : -1,
\r
158 'loopEndTime' : -1,
\r
159 'currentTime' : -1,
\r
163 'autoplay' : false,
\r
165 'source' : this[ 'source' ],
\r
170 pair && pair.setState( obj );
\r
175 * @alias Audio.prototype.loop
\r
176 * @param {boolean} v
\r
179 'loop' : function( v ){
\r
180 var pair = X_Pair_get( this );
\r
181 pair && pair.loop( v );
\r
185 * ボリュームの setter 実装不十分!
\r
186 * @alias Audio.prototype.volume
\r
187 * @param {number} v 0~1
\r
190 'volume' : function( v ){
\r
191 var pair = X_Pair_get( this );
\r
192 pair && pair.volume( v );
\r
197 * @alias Audio.prototype.currentTime
\r
198 * @param {number} v msで
\r
201 'currentTime' : function( v ){
\r
202 var pair = X_Pair_get( this );
\r
203 pair && pair.currentTime( v );
\r
208 * @alias Audio.prototype.isPlaying
\r
209 * @return {boolean}
\r
211 'isPlaying' : function(){
\r
212 var pair = X_Pair_get( this );
\r
213 return pair && pair.playing;
\r
219 function X_Audio_handleEvent( e ){
\r
220 var backend, src, pair;
\r
223 case X_EVENT_BACKEND_READY :
\r
224 backend = X_Audio_BACKENDS[ e[ 'backendID' ] ];
\r
226 this[ 'unlisten' ]( X_EVENT_BACKEND_NONE, X_Audio_handleEvent );
\r
227 this[ 'source' ] = e[ 'source' ];
\r
228 this[ 'backendName' ] = backend.backendName;
\r
230 X_Pair_create( this, backend.klass( this, e[ 'source' ], e[ 'option' ] ) );
\r
231 this[ 'listenOnce' ]( X_EVENT_READY, X_Audio_handleEvent );
\r
234 case X_EVENT_READY : // TODO AudioBase 側へ行かない?
\r
235 pair = X_Pair_get( this );
\r
236 ( pair.autoplay || pair._playReserved ) && pair.actualPlay();
\r
237 delete pair._playReserved;
\r
240 case X_EVENT_BACKEND_NONE :
\r
241 case X_EVENT_UNLOAD :
\r
245 case X_EVENT_KILL_INSTANCE :
\r
246 X_ViewPort[ 'unlisten' ]( X_EVENT_UNLOAD, this, X_Audio_handleEvent );
\r
247 if( backend = X_Pair_get( this ) ){
\r
248 backend[ 'kill' ]();
\r
249 X_Pair_release( this, backend );
\r
257 * TODO preplayerror play してみたら error が出た、backend の変更。
\r
260 function X_Audio_startDetectionBackend( backend, xaudio, sourceList, option ){
\r
261 var source = sourceList[ 0 ] || '',
\r
262 hash = X_URL_paramToObj( X_URL_getHash( source ) ),
\r
263 ext = hash[ 'ext' ] || X_URL_getEXT( source ),
\r
266 if( source && backend ){
\r
267 sup = [ xaudio, sourceList, option, source, ext ];
\r
270 xaudio[ 'listenOnce' ]( X_EVENT_COMPLETE, backend, X_Audio_onEndedDetection, sup );
\r
271 backend.detect( xaudio, ext, hash );
\r
273 xaudio[ 'asyncDispatch' ]( X_EVENT_BACKEND_NONE );
\r
277 function X_Audio_onEndedDetection( e, xaudio, sourceList, option, source, ext, sup ){
\r
278 var i = X_Audio_BACKENDS.indexOf( this ), _e, hash, backend;
\r
282 type : X_EVENT_BACKEND_READY,
\r
285 'backendName' : this.backendName,
\r
289 if( this.backendID === 1 ) _e[ 'needTouchForPlay' ] = /* X_WebAudio_need1stTouch && */ X_WebAudio_isNoTouch;
\r
291 if( this.backendID === 2 ) _e[ 'needTouchForLoad' ] = X_HTMLAudio_need1stTouch;
\r
293 xaudio[ 'asyncDispatch' ]( _e );
\r
295 console.log( 'No ' + source + ' ' + this.backendName );
\r
296 if( sup[ 3 ] = source = sourceList[ sourceList.indexOf( source ) + 1 ] ){
\r
297 hash = X_URL_paramToObj( X_URL_getHash( source ) );
\r
298 sup[ 4 ] = ext = hash[ 'ext' ] || X_URL_getEXT( source );
\r
299 xaudio[ 'listenOnce' ]( X_EVENT_COMPLETE, this, X_Audio_onEndedDetection, sup );
\r
300 this.detect( xaudio, ext, hash );
\r
302 if( backend = X_Audio_BACKENDS[ i + 1 ] ){
\r
303 X_Audio_startDetectionBackend( backend, xaudio, sourceList, option );
\r
305 xaudio[ 'asyncDispatch' ]( X_EVENT_BACKEND_NONE );
\r
312 var X_AudioBase = X_EventDispatcher[ 'inherits' ](
\r
318 startTime : 0, // state_startTime
\r
319 endTime : -1, // state_startTime
\r
320 loopStartTime : -1,
\r
329 autoplay : false,//
\r
332 _playReserved : false,
\r
334 play : function( startTime, endTime, loop, loopStartTime, loopEndTime ){
\r
335 if( 0 <= startTime ){
\r
337 'currentTime' : startTime,
\r
338 'startTime' : startTime,
\r
339 'endTime' : endTime,
\r
342 'loopStartTime' : loopStartTime,
\r
343 'loopEndTime' : loopEndTime
\r
346 // canPlay() : autoplay = true
\r
350 seek : function( seekTime ){
\r
351 if( seekTime < X_Audio_getEndTime( this ) ){
\r
352 this.setState( { 'currentTime' : seekTime } );
\r
356 pause : function(){
\r
357 this.seekTime = this.getActualCurrentTime();
\r
358 this.playing && this.actualPause();
\r
359 // delete this.autoplay
\r
360 // delete this.playing
\r
363 loop : function( v ){
\r
364 if( v === undefined ){
\r
365 return this.autoLoop;
\r
367 this.setState( { 'loop' : v } );
\r
370 volume : function( v ){
\r
371 if( v === undefined ){
\r
374 this.setState( { 'volume' : v } );
\r
377 currentTime : function( v ){
\r
378 if( v === undefined ){
\r
379 return this.playing ? this.getActualCurrentTime() : this.seekTime;
\r
381 this.setState( { 'currentTime' : v } );
\r
384 getState : function(){
\r
387 'startTime' : this.startTime,
\r
388 'endTime' : this.endTime < 0 ? this.duration : this.endTime,
\r
389 'loopStartTime' : this.loopStartTime < 0 ? this.startTime : this.loopStartTime,
\r
390 'loopEndTime' : this.loopEndTime < 0 ? ( this.endTime || this.duration ) : this.loopEndTime,
\r
391 'loop' : this.autoLoop,
\r
392 'looped' : this.looped,
\r
393 'volume' : this.gain,
\r
394 'playing' : this.playing,
\r
395 'duration' : this.duration,
\r
396 'autoplay' : this.autoplay,
\r
398 'currentTime' : this.playing ? this.getActualCurrentTime() : this.seekTime,
\r
399 'error' : this.getActualError ? this.getActualError() : this.error
\r
403 setState : function( obj ){
\r
404 var playing = this.playing,
\r
406 end = 0, seek = 0, volume = 0;
\r
411 case 'currentTime' :
\r
412 v = X_Audio_timeStringToNumber( v );
\r
413 if( X_Type_isNumber( v ) ){
\r
415 if( this.getActualCurrentTime() !== v ){
\r
428 v = X_Audio_timeStringToNumber( v );
\r
429 if( v || v === 0 ){
\r
430 if( this.startTime !== v ){
\r
431 this.startTime = v;
\r
434 delete this.startTime;
\r
439 v = X_Audio_timeStringToNumber( v );
\r
440 if( v || v === 0 ){
\r
441 if( this.endTime !== v ){
\r
443 if( playing ) end = 1;
\r
446 delete this.endTime;
\r
447 if( playing ) end = 1;
\r
451 case 'loopStartTime' :
\r
452 v = X_Audio_timeStringToNumber( v );
\r
453 if( v || v === 0 ){
\r
454 if( this.loopStartTime !== v ){
\r
455 this.loopStartTime = v;
\r
458 delete this.loopStartTime;
\r
462 case 'loopEndTime' :
\r
463 v = X_Audio_timeStringToNumber( v );
\r
464 if( v || v === 0 ){
\r
465 if( this.loopEndTime !== v ){
\r
466 this.loopEndTime = v;
\r
467 if( playing ) end = 1;
\r
470 delete this.loopEndTime;
\r
471 if( playing ) end = 1;
\r
476 if( X_Type_isBoolean( v ) && this.looped !== v ){
\r
478 if( playing ) seek = 2;
\r
483 if( X_Type_isBoolean( v ) && this.autoLoop !== v ){
\r
489 if( X_Type_isBoolean( v ) && this.autoplay !== v ){
\r
495 if( X_Type_isNumber( v ) ){
\r
496 v = v < 0 ? 0 : 1 < v ? 1 : v;
\r
497 if( this.gain !== v ){
\r
499 // if playing -> update
\r
500 if( playing ) volume = 4;
\r
507 alert( 'bad arg! ' + k );
\r
511 if( this.endTime < this.startTime ||
\r
512 ( this.loopEndTime < 0 ? this.endTime : this.loopEndTime ) < ( this.loopStartTime < 0 ? this.startTime : this.loopStartTime ) ||
\r
513 X_Audio_getEndTime( this ) < this.seekTime// ||
\r
514 //this.duration < this.endTime
\r
516 console.log( 'setState 0:' + this.startTime + ' -> ' + this.endTime + ' looped:' + this.looped + ' 1:' + this.loopStartTime + ' -> ' + this.loopEndTime );
\r
520 v = end + seek + volume;
\r
521 return v && this.playing && this.afterUpdateState( v );
\r
528 function X_Audio_timeStringToNumber( time ){
\r
529 var ary, ms, s = 0, m = 0, h = 0;
\r
531 if( X_Type_isNumber( time ) ) return time;
\r
532 if( !X_Type_isString( time ) || !time.length ) return;
\r
534 ary = time.split( '.' );
\r
535 ms = parseFloat( ( ary[ 1 ] + '000' ).substr( 0, 3 ) ) || 0;
\r
537 ary = ary[ 0 ].split( ':' );
\r
538 if( 3 < ary.length ) return;
\r
540 switch( ary.length ){
\r
544 s = parseFloat( ary[ 0 ] ) || 0;
\r
547 m = parseFloat( ary[ 0 ] ) || 0;
\r
548 s = parseFloat( ary[ 1 ] ) || 0;
\r
549 if( 60 <= s ) alert( 'invalid time string ' + time );
\r
552 h = parseFloat( ary[ 0 ] ) || 0;
\r
553 m = parseFloat( ary[ 1 ] ) || 0;
\r
554 s = parseFloat( ary[ 2 ] ) || 0;
\r
555 if( 60 <= s ) alert( 'invalid time string ' + time );
\r
556 if( 60 <= m ) alert( 'invalid time string ' + time );
\r
559 alert( 'invalid time string ' + time );
\r
561 ms = ( h * 3600 + m * 60 + s ) * 1000 + ms;
\r
562 return ms < 0 ? 0 : ms;
\r
565 function X_Audio_getStartTime( audioBase, endTime, delSeekTime ){
\r
566 var seek = audioBase.seekTime;
\r
568 if( delSeekTime ) delete audioBase.seekTime;
\r
571 if( audioBase.duration <= seek || endTime < seek ) return 0;
\r
575 if( audioBase.looped && 0 <= audioBase.loopStartTime ){
\r
576 if( audioBase.duration <= audioBase.loopStartTime || endTime < audioBase.loopStartTime ) return 0;
\r
577 return audioBase.loopStartTime;
\r
580 if( audioBase.startTime < 0 || audioBase.duration <= audioBase.startTime ) return 0;
\r
581 return audioBase.startTime;
\r
584 function X_Audio_getEndTime( audioBase ){
\r
585 var duration = audioBase.duration;
\r
587 if( audioBase.looped && 0 <= audioBase.loopEndTime ){
\r
588 if( duration <= audioBase.loopEndTime ) return duration;
\r
589 return audioBase.loopEndTime;
\r
592 if( audioBase.endTime < 0 || duration <= audioBase.endTime ) return duration;
\r
593 return audioBase.endTime;
\r