/** * A SoundInstance plays a {@link Sound}. * * One instance is one playback. It wraps an `AudioBufferSourceNode`, available as {@link source} * once playing, with a gain for {@link volume}, and carries {@link pitch}, {@link loop}, * {@link startTime} and {@link duration} to select the region of the sound it plays. {@link play}, * {@link pause}, {@link resume} and {@link stop} drive it and fire the events of the same names, * with `end` fired when playback finishes on its own; {@link isPlaying}, {@link isPaused} and * {@link isStopped} report the state, and {@link currentTime} can be read or set to seek. * Instances are normally created by {@link SoundSlot#play} on a {@link SoundComponent}, which * returns the instance so a script can adjust or stop that one playback. {@link setExternalNodes} * inserts Web Audio nodes such as filters between the source and the destination. * * @example * const instance = entity.sound.play('engine'); * instance.pitch = 1.5; * instance.once('end', () => console.log('finished')); * @category Sound */ export class SoundInstance extends EventHandler { /** * Fired when the instance starts playing its source. * * @event * @example * instance.on('play', () => { * console.log('Instance started playing'); * }); */ static EVENT_PLAY: string; /** * Fired when the instance is paused. * * @event * @example * instance.on('pause', () => { * console.log('Instance paused'); * }); */ static EVENT_PAUSE: string; /** * Fired when the instance is resumed. * * @event * @example * instance.on('resume', () => { * console.log('Instance resumed'); * }); */ static EVENT_RESUME: string; /** * Fired when the instance is stopped. * * @event * @example * instance.on('stop', () => { * console.log('Instance stopped'); * }); */ static EVENT_STOP: string; /** * Fired when the sound currently played by the instance ends. * * @event * @example * instance.on('end', () => { * console.log('Instance ended'); * }); */ static EVENT_END: string; /** * Create a new SoundInstance instance. * * @param {SoundManager} manager - The sound manager. * @param {Sound} sound - The sound to play. * @param {object} options - Options for the instance. * @param {number} [options.volume] - The playback volume, between 0 and 1. Defaults to 1. * @param {number} [options.pitch] - The relative pitch. Defaults to 1 (plays at normal pitch). * @param {boolean} [options.loop] - Whether the sound should loop when it reaches the end or * not. Defaults to false. * @param {number} [options.startTime] - The time from which the playback will start in * seconds. Default is 0 to start at the beginning. Defaults to 0. * @param {number} [options.duration] - The total time after the startTime in seconds when * playback will stop or restart if loop is true. Defaults to 0. * @param {Function} [options.onPlay] - Function called when the instance starts playing. * @param {Function} [options.onPause] - Function called when the instance is paused. * @param {Function} [options.onResume] - Function called when the instance is resumed. * @param {Function} [options.onStop] - Function called when the instance is stopped. * @param {Function} [options.onEnd] - Function called when the instance ends. */ constructor(manager: SoundManager, sound: Sound, options: { volume?: number; pitch?: number; loop?: boolean; startTime?: number; duration?: number; onPlay?: Function; onPause?: Function; onResume?: Function; onStop?: Function; onEnd?: Function; }); /** * Gets the source that plays the sound resource. Source is only available after calling play. * * @type {AudioBufferSourceNode|null} */ source: AudioBufferSourceNode | null; /** * @type {SoundManager} * @private */ private _manager; /** * @type {number} * @private */ private _volume; /** * @type {number} * @private */ private _pitch; /** @private */ private _loop; /** * @type {Sound} * @private */ private _sound; /** * Start at 'stopped'. * * @type {number} * @private */ private _state; /** * True if the manager was suspended. * * @private */ private _suspended; /** * Greater than 0 if we want to suspend the event handled to the 'onended' event. * When an 'onended' event is suspended, this counter is decremented by 1. * When a future 'onended' event is to be suspended, this counter is incremented by 1. * * @private */ private _suspendEndEvent; /** * True if we want to suspend firing instance events. * * @private */ private _suspendInstanceEvents; /** * If true then the instance will start playing its source when its created. * * @private */ private _playWhenLoaded; /** @private */ private _startTime; /** @private */ private _duration; /** * @type {number|null} * @private */ private _startOffset; /** @private */ private _onPlayCallback; /** @private */ private _onPauseCallback; /** @private */ private _onResumeCallback; /** @private */ private _onStopCallback; /** @private */ private _onEndCallback; /** @private */ private _startedAt; /** * Manually keep track of the playback position because the Web Audio API does not * provide a way to do this accurately if the playbackRate is not 1. * * @private */ private _currentTime; /** * The playback position relative to startTime when _startedAt was recorded. * * @private */ private _currentOffset; /** * The offset into the buffer, in seconds, that the source was playing at when the position * tracking used by the loop region was last brought up to date. * * @private */ private _sourceOffset; /** * The context time that _sourceOffset was recorded at. * * @private */ private _sourceOffsetAt; /** * Whether the source has been looping since _sourceOffset was recorded. * * @private */ private _sourceLooping; /** * Whether a stop at the end of the loop region is currently scheduled on the source. * * @private */ private _regionStopPending; /** * The input node is the one that is connected to the source. * * @type {AudioNode|null} * @private */ private _inputNode; /** * The connected node is the one that is connected to the destination (speakers). Any * external nodes will be connected to this node. * * @type {AudioNode|null} * @private */ private _connectorNode; /** * The first external node set by a user. * * @type {AudioNode|null} * @private */ private _firstNode; /** * The last external node set by a user. * * @type {AudioNode|null} * @private */ private _lastNode; /** * Set to true if a play() request was issued when the AudioContext was still suspended, * and will therefore wait until it is resumed to play the audio. * * @private */ private _waitingContextSuspension; /** @private */ private _endedHandler; /** * Sets the current time of the sound that is playing, relative to {@link startTime}. If the * value provided is bigger than the duration of the instance it will wrap from the beginning. * * @type {number} */ set currentTime(value: number); /** * Gets the current time of the sound that is playing, relative to {@link startTime}. * * @type {number} */ get currentTime(): number; /** * Sets the duration of the sound that the instance will play starting from startTime. * * @type {number} */ set duration(value: number); /** * Gets the duration of the sound that the instance will play starting from {@link startTime}. * The returned value is clamped to the time available after the normalized start time. * * @type {number} */ get duration(): number; /** * Gets whether the instance is currently paused. * * @type {boolean} */ get isPaused(): boolean; /** * Gets whether the instance is currently playing. * * @type {boolean} */ get isPlaying(): boolean; /** * Gets whether the instance is currently stopped. * * @type {boolean} */ get isStopped(): boolean; /** * Gets whether the instance is currently suspended because the window is not focused. * * @type {boolean} */ get isSuspended(): boolean; /** * Sets whether the instance will restart when it finishes playing. * * @type {boolean} */ set loop(value: boolean); /** * Gets whether the instance will restart when it finishes playing. * * @type {boolean} */ get loop(): boolean; /** * Sets the pitch modifier to play the sound with. Must be larger than 0.01. * * @type {number} */ set pitch(pitch: number); /** * Gets the pitch modifier to play the sound with. * * @type {number} */ get pitch(): number; /** * Sets the sound resource that the instance will play. * * @type {Sound} */ set sound(value: Sound); /** * Gets the sound resource that the instance will play. * * @type {Sound} */ get sound(): Sound; /** * Sets the start time from which the sound will start playing. * * @type {number} */ set startTime(value: number); /** * Gets the start time from which the sound will start playing. * * @type {number} */ get startTime(): number; /** * Sets the volume modifier to play the sound with. In range 0-1. * * @type {number} */ set volume(volume: number); /** * Gets the volume modifier to play the sound with. In range 0-1. * * @type {number} */ get volume(): number; /** @private */ private _onPlay; /** @private */ private _onPause; /** @private */ private _onResume; /** @private */ private _onStop; /** @private */ private _onEnded; /** * Handle the manager's 'volumechange' event. * * @private */ private _onManagerVolumeChange; /** * Handle the manager's 'suspend' event. * * @private */ private _onManagerSuspend; /** * Handle the manager's 'resume' event. * * @private */ private _onManagerResume; /** * Creates internal audio nodes and connects them. * * @private */ private _initializeNodes; gain: GainNode; /** * Attempt to begin playback the sound. * If the AudioContext is suspended, the audio will only start once it's resumed. * If the sound is already playing, this will restart the sound. * * @returns {boolean} True if the sound was started immediately. */ play(): boolean; /** * Immediately play the sound. * This method assumes the AudioContext is ready (not suspended or locked). * * @private */ private _playAudioImmediate; /** * Starts the source at the specified offset into the buffer. * * The duration is only passed to the source for non-looping instances. For a looping instance, * the region to play is defined by loopStart/loopEnd - passing a duration as well would stop * playback at the end of the first iteration instead of looping. * * @param {number} offset - The offset into the buffer, in seconds, to start playing from. * @param {number} currentOffset - The offset relative to startTime, in seconds. * @private */ private _startSource; /** * Brings the tracked source position up to date and returns the offset into the buffer, in * seconds, that the source is currently playing at. Tracked separately from _currentTime * because that is capped to the duration of the instance rather than the buffer. * * @returns {number} The offset into the buffer, in seconds. * @private */ private _syncSourcePosition; /** * Schedules, reschedules or unschedules the stop that ends the loop region. A looping source is * started without a duration, so when looping is disabled the end of the current iteration has * to be scheduled manually - and that deadline is in context time, so it has to be revisited * whenever looping or the pitch changes. * * @private */ private _updateRegionStop; /** * Pauses playback of sound. Call resume() to resume playback from the same position. * * @returns {boolean} Returns true if the sound was paused. */ pause(): boolean; /** * Resumes playback of the sound. Playback resumes at the point that the audio was paused. * * @returns {boolean} Returns true if the sound was resumed. */ resume(): boolean; /** * Stops playback of sound. Calling play() again will restart playback from the beginning of * the sound. * * @returns {boolean} Returns true if the sound was stopped. */ stop(): boolean; /** * Connects external Web Audio API nodes. You need to pass the first node of the node graph * that you created externally and the last node of that graph. The first node will be * connected to the audio source and the last node will be connected to the destination of the * AudioContext (e.g. speakers). Requires Web Audio API support. * * @param {AudioNode} firstNode - The first node that will be connected to the audio source of sound instances. * @param {AudioNode} [lastNode] - The last node that will be connected to the destination of the AudioContext. * If unspecified then the firstNode will be connected to the destination instead. * @example * const context = app.systems.sound.context; * const analyzer = context.createAnalyzer(); * const distortion = context.createWaveShaper(); * const filter = context.createBiquadFilter(); * analyzer.connect(distortion); * distortion.connect(filter); * instance.setExternalNodes(analyzer, filter); */ setExternalNodes(firstNode: AudioNode, lastNode?: AudioNode): void; /** * Clears any external nodes set by {@link setExternalNodes}. */ clearExternalNodes(): void; /** * Gets any external nodes set by {@link setExternalNodes}. * * @returns {AudioNode[]} Returns an array that contains the two nodes set by * {@link setExternalNodes}. */ getExternalNodes(): AudioNode[]; /** * Creates the source for the instance. * * @returns {AudioBufferSourceNode|null} Returns the created source or null if the sound * instance has no {@link Sound} associated with it. * @private */ private _createSource; /** * Sets the current time taking into account the time the instance started playing, the current * pitch and the current time offset. * * @private */ private _updateCurrentTime; /** * Handle the manager's 'destroy' event. * * @private */ private _onManagerDestroy; } import { EventHandler } from '../../core/event-handler.js'; import type { Sound } from './sound.js'; import type { SoundManager } from './manager.js';