import logger from '@theatre/shared/logger' ; import { privateAPI , setPrivateAPI } from '@theatre/core/privateAPIs' ; import { defer } from '@theatre/shared/utils/defer' ; import type Sequence from './Sequence' ; import type { IPlaybackDirection , IPlaybackRange } from './Sequence' ; import AudioPlaybackController from './playbackControllers/AudioPlaybackController' ; import coreTicker from '@theatre/core/coreTicker' ; import type { Pointer } from '@theatre/dataverse' ; interface IAttachAudioArgs { /**
     * Either a URL to the audio file (eg "https://localhost/audio.mp3") or an instance of AudioBuffer
     */ source : string | AudioBuffer ; /**
     * An optional AudioContext. If not provided, one will be created.
     */ audioContext ? : AudioContext ; /**
     * An AudioNode to feed the audio into. Will use audioContext.destination if not provided.
     */ destinationNode ? : AudioNode ; } export interface ISequence { readonly type : 'Theatre_Sequence_PublicAPI' ; returns example play ( conf ? ) : Promise < boolean > ; /**
     * Pauses the currently playing animation
     */ pause ( ) : void ; /**
     * The current position of the playhead.
     * In a time-based sequence, this represents the current time in seconds.
     */ position : number ; remarks As with any Pointer, you can use this with  onChange to listen to its value changes
     * or with  val to read its current value.
     *
     *  example pointer : Pointer < { playing : boolean ; length : number ; position : number ; } > ; returns example example attachAudio ( args ) : Promise < { /**
         * An  https .
         * If `args.source` is a URL, then `decodedBuffer` would be the result
         * of  https * on the audio file at that URL.
         *
         * If `args.source` is an `AudioBuffer`, then `decodedBuffer` would be equal to `args.source`
          decodedBuffer : AudioBuffer ; /**
         * The `AudioContext`. It is either equal to `source.audioContext` if it is provided, or
         * one that's created on the fly.
         */ audioContext : AudioContext ; /**
         * Equals to either `args.destinationNode`, or if none is provided, it equals `audioContext.destinationNode`.
         *
         * See `gainNode` for more info.
         */ destinationNode : AudioNode ; example gainNode : GainNode ; } > ; } export default class TheatreSequence implements ISequence { get type ( ) : 'Theatre_Sequence_PublicAPI' { return 'Theatre_Sequence_PublicAPI' ; } internal constructor ( sheet ) { setPrivateAPI ( this , sheet ) ; } play ( conf ? ) { if ( privateAPI ( this ) . _project . isReady ( ) ) { return privateAPI ( this ) . play ( conf ) ; } else { if ( process . env . NODE_ENV !== 'production' ) { logger . warn ( `You seem to have called sequence.play() before the project has finished loading.\n` + `This would **not** a problem in production when using '@theatre/core', since Theatre loads instantly in core mode. ` + `However, when using '@theatre/studio', it takes a few milliseconds for it to load your project's state, ` + `before which your sequences cannot start playing.\n` + `\n` + `To fix this, simply defer calling sequence.play() until after the project is loaded, like this:\n` + `project.ready.then(() => {\n` + `  sequence.play()\n` + `})` ) ; } const d = defer < boolean > ( ) ; d . resolve ( true ) ; return d . promise ; } } pause ( ) { privateAPI ( this ) . pause ( ) ; } get position ( ) { return privateAPI ( this ) . position ; } set position ( position ) { privateAPI ( this ) . position = position ; } async attachAudio ( args ) { const { audioContext , destinationNode , decodedBuffer , gainNode } = await resolveAudioBuffer ( args ) ; const playbackController = new AudioPlaybackController ( coreTicker , decodedBuffer , audioContext , gainNode ) ; privateAPI ( this ) . replacePlaybackController ( playbackController ) ; return { audioContext , destinationNode , decodedBuffer , gainNode } ; } get pointer ( ) : ISequence [ 'pointer' ] { return privateAPI ( this ) . pointer ; } } async function resolveAudioBuffer ( args ) { function getAudioContext ( ) { if ( args . audioContext ) return Promise . resolve ( args . audioContext ) ; const ctx = new AudioContext ( ) ; if ( ctx . state === 'running' ) return Promise . resolve ( ctx ) ; return new Promise < AudioContext > ( ( resolve ) => { const listener = ( ) => { ctx . resume ( ) ; } ; const eventsToHookInto = [ 'mousedown' , 'keydown' , 'touchstart' , ] ; const eventListenerOpts = { capture : true , passive : false } ; eventsToHookInto . forEach ( ( eventName ) => { window . addEventListener ( eventName , listener , eventListenerOpts ) ; } ) ; ctx . addEventListener ( 'statechange' , ( ) => { if ( ctx . state === 'running' ) { eventsToHookInto . forEach ( ( eventName ) => { window . removeEventListener ( eventName , listener , eventListenerOpts ) ; } ) ; resolve ( ctx ) ; } } ) ; } ) ; } async function getAudioBuffer ( ) { if ( args . source instanceof AudioBuffer ) { return args . source ; } const decodedBufferDeferred = defer < AudioBuffer > ( ) ; if ( typeof args . source !== 'string' ) { throw new Error ( `Error validating arguments to sequence.attachAudio(). ` + `args.source must either be a string or an instance of AudioBuffer.` ) ; } let fetchResponse ; try { fetchResponse = await fetch ( args . source ) ; } catch ( e ) { console . error ( e ) ; throw new Error ( `Could not fetch '${ args . source }'. Network error logged above.` ) ; } let arrayBuffer ; try { arrayBuffer = await fetchResponse . arrayBuffer ( ) ; } catch ( e ) { console . error ( e ) ; throw new Error ( `Could not read '${ args . source }' as an arrayBuffer.` ) ; } const audioContext = await audioContextPromise ; audioContext . decodeAudioData ( arrayBuffer , decodedBufferDeferred . resolve , decodedBufferDeferred . reject ) ; let decodedBuffer ; try { decodedBuffer = await decodedBufferDeferred . promise ; } catch ( e ) { console . error ( e ) ; throw new Error ( `Could not decode ${ args . source } as an audio file.` ) ; } return decodedBuffer ; } const audioContextPromise = getAudioContext ( ) ; const audioBufferPromise = getAudioBuffer ( ) ; const [ audioContext , decodedBuffer ] = await Promise . all ( [ audioContextPromise , audioBufferPromise , ] ) ; const destinationNode = args . destinationNode || audioContext . destination ; const gainNode = audioContext . createGain ( ) ; gainNode . connect ( destinationNode ) ; return { audioContext , decodedBuffer , gainNode , destinationNode , } ; }