import { Ticker } from '@pixi/ticker' ; import { EventEmitter } from '@pixi/utils' ; import { IMediaInstance } from '../interfaces' ; import { PlayOptions } from '../Sound' ; import { WebAudioMedia } from './WebAudioMedia' ; import { WebAudioUtils } from './WebAudioUtils' ; import { Filter } from '../filters/Filter' ; let id = 0 ; memberof extends PIXI . utils . EventEmitter class WebAudioInstance extends EventEmitter implements IMediaInstance { readonly public readonly id : number ; /** The source Sound. */ private _media : WebAudioMedia ; /** true if paused. */ private _paused : boolean ; /** true if muted. */ private _muted : boolean ; /** true if paused. */ private _pausedReal : boolean ; /** The instance volume */ private _volume : number ; /** Last update frame number. */ private _lastUpdate : number ; /** The total number of seconds elapsed in playback. */ private _elapsed : number ; /** Playback rate, where 1 is 100%. */ private _speed : number ; /** Playback rate, where 1 is 100%. */ private _end : number ; /** `true` if should be looping. */ private _loop : boolean ; /** Gain node for controlling volume of instance */ private _gain : GainNode ; /** Length of the sound in seconds. */ private _duration : number ; /** The progress of the sound from 0 to 1. */ private _progress : number ; /** Audio buffer source clone from Sound object. */ private _source : AudioBufferSourceNode ; /** The filters */ private _filters : Filter [ ] ; constructor ( media ) { super ( ) ; this . id = id ++ ; this . _media = null ; this . _paused = false ; this . _muted = false ; this . _elapsed = 0 ; this . init ( media ) ; } param name param value public set ( name , value ) { if ( this [ name ] === undefined ) { throw new Error ( `Property with name ${ name } does not exist.` ) ; } else { switch ( name ) { case 'speed' : this . speed = value as number ; break ; case 'volume' : this . volume = value as number ; break ; case 'muted' : this . muted = value as boolean ; break ; case 'loop' : this . loop = value as boolean ; break ; case 'paused' : this . paused = value as boolean ; break ; } } return this ; } /** Stops the instance, don't use after this. */ public stop ( ) { if ( this . _source ) { this . _internalStop ( ) ; this . emit ( 'stop' ) ; } } /** Set the instance speed from 0 to 1 */ public get speed ( ) : number { return this . _speed ; } public set speed ( speed ) { this . _speed = speed ; this . refresh ( ) ; this . _update ( true ) ; } /** Get the set the volume for this instance from 0 to 1 */ public get volume ( ) : number { return this . _volume ; } public set volume ( volume ) { this . _volume = volume ; this . refresh ( ) ; } /** `true` if the sound is muted */ public get muted ( ) : boolean { return this . _muted ; } public set muted ( muted ) { this . _muted = muted ; this . refresh ( ) ; } /** If the sound instance should loop playback */ public get loop ( ) : boolean { return this . _loop ; } public set loop ( loop ) { this . _loop = loop ; this . refresh ( ) ; } /** The collection of filters. */ public get filters ( ) : Filter [ ] { return this . _filters ; } public set filters ( filters ) { if ( this . _filters ) { this . _filters ?. filter ( ( filter ) => filter ) . forEach ( ( filter ) => filter . disconnect ( ) ) ; this . _filters = null ; this . _source . connect ( this . _gain ) ; } this . _filters = filters ?. length ? filters . slice ( 0 ) : null ; this . refresh ( ) ; } /** Refresh loop, volume and speed based on changes to parent */ public refresh ( ) { if ( ! this . _source ) { return ; } const global = this . _media . context ; const sound = this . _media . parent ; this . _source . loop = this . _loop || sound . loop ; const globalVolume = global . volume * ( global . muted ? 0 : 1 ) ; const soundVolume = sound . volume * ( sound . muted ? 0 : 1 ) ; const instanceVolume = this . _volume * ( this . _muted ? 0 : 1 ) ; WebAudioUtils . setParamValue ( this . _gain . gain , instanceVolume * soundVolume * globalVolume ) ; WebAudioUtils . setParamValue ( this . _source . playbackRate , this . _speed * sound . speed * global . speed ) ; this . applyFilters ( ) ; } /** Connect filters nodes to audio context */ private applyFilters ( ) { if ( this . _filters ?. length ) { this . _source . disconnect ( ) ; let source = this . _source ; this . _filters . forEach ( ( filter ) => { source . connect ( filter . destination ) ; source = filter ; } ) ; source . connect ( this . _gain ) ; } } /** Handle changes in paused state, either globally or sound or instance */ public refreshPaused ( ) { const global = this . _media . context ; const sound = this . _media . parent ; const pausedReal = this . _paused || sound . paused || global . paused ; if ( pausedReal !== this . _pausedReal ) { this . _pausedReal = pausedReal ; if ( pausedReal ) { this . _internalStop ( ) ; event this . emit ( 'paused' ) ; } else { event this . emit ( 'resumed' ) ; this . play ( { start : this . _elapsed % this . _duration , end : this . _end , speed : this . _speed , loop : this . _loop , volume : this . _volume , } ) ; } event property this . emit ( 'pause' , pausedReal ) ; } } param options public play ( options ) { const { start , end , speed , loop , volume , muted , filters } = options ; if ( end ) { console . assert ( end > start , 'End time is before start time' ) ; } this . _paused = false ; const { source , gain } = this . _media . nodes . cloneBufferSource ( ) ; this . _source = source ; this . _gain = gain ; this . _speed = speed ; this . _volume = volume ; this . _loop = ! ! loop ; this . _muted = muted ; this . _filters = filters ; this . refresh ( ) ; const duration = this . _source . buffer . duration ; this . _duration = duration ; this . _end = end ; this . _lastUpdate = this . _now ( ) ; this . _elapsed = start ; this . _source . onended = this . _onComplete . bind ( this ) ; if ( this . _loop ) { this . _source . loopEnd = end ; this . _source . loopStart = start ; this . _source . start ( 0 , start ) ; } else if ( end ) { this . _source . start ( 0 , start , end - start ) ; } else { this . _source . start ( 0 , start ) ; } event this . emit ( 'start' ) ; this . _update ( true ) ; this . enableTicker ( true ) ; } /** Start the update progress. */ private enableTicker ( enabled ) { Ticker . shared . remove ( this . _updateListener , this ) ; if ( enabled ) { Ticker . shared . add ( this . _updateListener , this ) ; } } /** The current playback progress from 0 to 1. */ public get progress ( ) : number { return this . _progress ; } /** Pauses the sound. */ public get paused ( ) : boolean { return this . _paused ; } public set paused ( paused ) { this . _paused = paused ; this . refreshPaused ( ) ; } /** Don't use after this. */ public destroy ( ) { this . removeAllListeners ( ) ; this . _internalStop ( ) ; if ( this . _gain ) { this . _gain . disconnect ( ) ; this . _gain = null ; } if ( this . _media ) { this . _media . context . events . off ( 'refresh' , this . refresh , this ) ; this . _media . context . events . off ( 'refreshPaused' , this . refreshPaused , this ) ; this . _media = null ; } this . _filters ?. forEach ( ( filter ) => filter . disconnect ( ) ) ; this . _filters = null ; this . _end = null ; this . _speed = 1 ; this . _volume = 1 ; this . _loop = false ; this . _elapsed = 0 ; this . _duration = 0 ; this . _paused = false ; this . _muted = false ; this . _pausedReal = false ; } return public toString ( ) { return `[WebAudioInstance id=${ this . id }]` ; } return private _now ( ) { return this . _media . context . audioContext . currentTime ; } /** Callback for update listener */ private _updateListener ( ) { this . _update ( ) ; } /** Internal update the progress. */ private _update ( force = false ) { if ( this . _source ) { const now = this . _now ( ) ; const delta = now - this . _lastUpdate ; if ( delta > 0 || force ) { const speed = this . _source . playbackRate . value ; this . _elapsed += delta * speed ; this . _lastUpdate = now ; const duration = this . _duration ; let progress ; if ( this . _source . loopStart ) { const soundLength = this . _source . loopEnd - this . _source . loopStart ; progress = ( this . _source . loopStart + ( this . _elapsed % soundLength ) ) / duration ; } else { progress = ( this . _elapsed % duration ) / duration ; } this . _progress = progress ; event property property this . emit ( 'progress' , this . _progress , duration ) ; } } } /** Initializes the instance. */ public init ( media ) { this . _media = media ; media . context . events . on ( 'refresh' , this . refresh , this ) ; media . context . events . on ( 'refreshPaused' , this . refreshPaused , this ) ; } /** Stops the instance. */ private _internalStop ( ) { if ( this . _source ) { this . enableTicker ( false ) ; this . _source . onended = null ; this . _source . stop ( 0 ) ; this . _source . disconnect ( ) ; try { this . _source . buffer = null ; } catch ( err ) { console . warn ( 'Failed to set AudioBufferSourceNode.buffer to null:' , err ) ; } this . _source = null ; } } /** Callback when completed. */ private _onComplete ( ) { if ( this . _source ) { this . enableTicker ( false ) ; this . _source . onended = null ; this . _source . disconnect ( ) ; try { this . _source . buffer = null ; } catch ( err ) { console . warn ( 'Failed to set AudioBufferSourceNode.buffer to null:' , err ) ; } } this . _source = null ; this . _progress = 1 ; this . emit ( 'progress' , 1 , this . _duration ) ; event this . emit ( 'end' , this ) ; } } export { WebAudioInstance } ;