UNPKG

@openhps/core

Version:

Open Hybrid Positioning System - Core component

617 lines (572 loc) 15.9 kB
import { Object3D } from '../core/Object3D.js'; /** * Represents a non-positional ( global ) audio object. * * This and related audio modules make use of the [Web Audio API]{@link https://www.w3.org/TR/webaudio-1.1/}. * * ```js * // create an AudioListener and add it to the camera * const listener = new THREE.AudioListener(); * camera.add( listener ); * * // create a global audio source * const sound = new THREE.Audio( listener ); * * // load a sound and set it as the Audio object's buffer * const audioLoader = new THREE.AudioLoader(); * audioLoader.load( 'sounds/ambient.ogg', function( buffer ) { * sound.setBuffer( buffer ); * sound.setLoop( true ); * sound.setVolume( 0.5 ); * sound.play(); * }); * ``` * * @augments Object3D */ class Audio extends Object3D { /** * Constructs a new audio. * * @param {AudioListener} listener - The global audio listener. */ constructor(listener) { super(); this.type = 'Audio'; /** * The global audio listener. * * @type {AudioListener} * @readonly */ this.listener = listener; /** * The audio context. * * @type {AudioContext} * @readonly */ this.context = listener.context; /** * The gain node used for volume control. * * @type {GainNode} * @readonly */ this.gain = this.context.createGain(); this.gain.connect(listener.getInput()); /** * Whether to start playback automatically or not. * * @type {boolean} * @default false */ this.autoplay = false; /** * A reference to an audio buffer. * * Defined via {@link Audio#setBuffer}. * * @type {?AudioBuffer} * @default null * @readonly */ this.buffer = null; /** * Modify pitch, measured in cents. +/- 100 is a semitone. * +/- 1200 is an octave. * * Defined via {@link Audio#setDetune}. * * @type {number} * @default 0 * @readonly */ this.detune = 0; /** * Whether the audio should loop or not. * * Defined via {@link Audio#setLoop}. * * @type {boolean} * @default false * @readonly */ this.loop = false; /** * Defines where in the audio buffer the replay should * start, in seconds. * * @type {number} * @default 0 */ this.loopStart = 0; /** * Defines where in the audio buffer the replay should * stop, in seconds. * * @type {number} * @default 0 */ this.loopEnd = 0; /** * An offset to the time within the audio buffer the playback * should begin, in seconds. * * @type {number} * @default 0 */ this.offset = 0; /** * Overrides the default duration of the audio. * * @type {undefined|number} * @default undefined */ this.duration = undefined; /** * The playback speed. * * Defined via {@link Audio#setPlaybackRate}. * * @type {number} * @readonly * @default 1 */ this.playbackRate = 1; /** * Indicates whether the audio is playing or not. * * This flag will be automatically set when using {@link Audio#play}, * {@link Audio#pause}, {@link Audio#stop}. * * @type {boolean} * @readonly * @default false */ this.isPlaying = false; /** * Indicates whether the audio playback can be controlled * with method like {@link Audio#play} or {@link Audio#pause}. * * This flag will be automatically set when audio sources are * defined. * * @type {boolean} * @readonly * @default true */ this.hasPlaybackControl = true; /** * Holds a reference to the current audio source. * * The property is automatically by one of the `set*()` methods. * * @type {?AudioNode} * @readonly * @default null */ this.source = null; /** * Defines the source type. * * The property is automatically by one of the `set*()` methods. * * @type {('empty'|'audioNode'|'mediaNode'|'mediaStreamNode'|'buffer')} * @readonly * @default 'empty' */ this.sourceType = 'empty'; this._startedAt = 0; this._progress = 0; this._connected = false; /** * Can be used to apply a variety of low-order filters to create * more complex sound effects e.g. via `BiquadFilterNode`. * * The property is automatically set by {@link Audio#setFilters}. * * @type {Array<AudioNode>} * @readonly */ this.filters = []; } /** * Returns the output audio node. * * @return {GainNode} The output node. */ getOutput() { return this.gain; } /** * Sets the given audio node as the source of this instance. * * {@link Audio#sourceType} is set to `audioNode` and {@link Audio#hasPlaybackControl} to `false`. * * @param {AudioNode} audioNode - The audio node like an instance of `OscillatorNode`. * @return {Audio} A reference to this instance. */ setNodeSource(audioNode) { this.hasPlaybackControl = false; this.sourceType = 'audioNode'; this.source = audioNode; this.connect(); return this; } /** * Sets the given media element as the source of this instance. * * {@link Audio#sourceType} is set to `mediaNode` and {@link Audio#hasPlaybackControl} to `false`. * * @param {HTMLMediaElement} mediaElement - The media element. * @return {Audio} A reference to this instance. */ setMediaElementSource(mediaElement) { this.hasPlaybackControl = false; this.sourceType = 'mediaNode'; this.source = this.context.createMediaElementSource(mediaElement); this.connect(); return this; } /** * Sets the given media stream as the source of this instance. * * {@link Audio#sourceType} is set to `mediaStreamNode` and {@link Audio#hasPlaybackControl} to `false`. * * @param {MediaStream} mediaStream - The media stream. * @return {Audio} A reference to this instance. */ setMediaStreamSource(mediaStream) { this.hasPlaybackControl = false; this.sourceType = 'mediaStreamNode'; this.source = this.context.createMediaStreamSource(mediaStream); this.connect(); return this; } /** * Sets the given audio buffer as the source of this instance. * * {@link Audio#sourceType} is set to `buffer` and {@link Audio#hasPlaybackControl} to `true`. * * @param {AudioBuffer} audioBuffer - The audio buffer. * @return {Audio} A reference to this instance. */ setBuffer(audioBuffer) { this.buffer = audioBuffer; this.sourceType = 'buffer'; if (this.autoplay) this.play(); return this; } /** * Starts the playback of the audio. * * Can only be used with compatible audio sources that allow playback control. * * @param {number} [delay=0] - The delay, in seconds, at which the audio should start playing. * @return {Audio|undefined} A reference to this instance. */ play(delay = 0) { if (this.isPlaying === true) { console.warn('THREE.Audio: Audio is already playing.'); return; } if (this.hasPlaybackControl === false) { console.warn('THREE.Audio: this Audio has no playback control.'); return; } this._startedAt = this.context.currentTime + delay; const source = this.context.createBufferSource(); source.buffer = this.buffer; source.loop = this.loop; source.loopStart = this.loopStart; source.loopEnd = this.loopEnd; source.onended = this.onEnded.bind(this); source.start(this._startedAt, this._progress + this.offset, this.duration); this.isPlaying = true; this.source = source; this.setDetune(this.detune); this.setPlaybackRate(this.playbackRate); return this.connect(); } /** * Pauses the playback of the audio. * * Can only be used with compatible audio sources that allow playback control. * * @return {Audio|undefined} A reference to this instance. */ pause() { if (this.hasPlaybackControl === false) { console.warn('THREE.Audio: this Audio has no playback control.'); return; } if (this.isPlaying === true) { // update current progress this._progress += Math.max(this.context.currentTime - this._startedAt, 0) * this.playbackRate; if (this.loop === true) { // ensure _progress does not exceed duration with looped audios this._progress = this._progress % (this.duration || this.buffer.duration); } this.source.stop(); this.source.onended = null; this.isPlaying = false; } return this; } /** * Stops the playback of the audio. * * Can only be used with compatible audio sources that allow playback control. * * @param {number} [delay=0] - The delay, in seconds, at which the audio should stop playing. * @return {Audio|undefined} A reference to this instance. */ stop(delay = 0) { if (this.hasPlaybackControl === false) { console.warn('THREE.Audio: this Audio has no playback control.'); return; } this._progress = 0; if (this.source !== null) { this.source.stop(this.context.currentTime + delay); this.source.onended = null; } this.isPlaying = false; return this; } /** * Connects to the audio source. This is used internally on * initialisation and when setting / removing filters. * * @return {Audio} A reference to this instance. */ connect() { if (this.filters.length > 0) { this.source.connect(this.filters[0]); for (let i = 1, l = this.filters.length; i < l; i++) { this.filters[i - 1].connect(this.filters[i]); } this.filters[this.filters.length - 1].connect(this.getOutput()); } else { this.source.connect(this.getOutput()); } this._connected = true; return this; } /** * Disconnects to the audio source. This is used internally on * initialisation and when setting / removing filters. * * @return {Audio|undefined} A reference to this instance. */ disconnect() { if (this._connected === false) { return; } if (this.filters.length > 0) { this.source.disconnect(this.filters[0]); for (let i = 1, l = this.filters.length; i < l; i++) { this.filters[i - 1].disconnect(this.filters[i]); } this.filters[this.filters.length - 1].disconnect(this.getOutput()); } else { this.source.disconnect(this.getOutput()); } this._connected = false; return this; } /** * Returns the current set filters. * * @return {Array<AudioNode>} The list of filters. */ getFilters() { return this.filters; } /** * Sets an array of filters and connects them with the audio source. * * @param {Array<AudioNode>} [value] - A list of filters. * @return {Audio} A reference to this instance. */ setFilters(value) { if (!value) value = []; if (this._connected === true) { this.disconnect(); this.filters = value.slice(); this.connect(); } else { this.filters = value.slice(); } return this; } /** * Defines the detuning of oscillation in cents. * * @param {number} value - The detuning of oscillation in cents. * @return {Audio} A reference to this instance. */ setDetune(value) { this.detune = value; if (this.isPlaying === true && this.source.detune !== undefined) { this.source.detune.setTargetAtTime(this.detune, this.context.currentTime, 0.01); } return this; } /** * Returns the detuning of oscillation in cents. * * @return {number} The detuning of oscillation in cents. */ getDetune() { return this.detune; } /** * Returns the first filter in the list of filters. * * @return {AudioNode|undefined} The first filter in the list of filters. */ getFilter() { return this.getFilters()[0]; } /** * Applies a single filter node to the audio. * * @param {AudioNode} [filter] - The filter to set. * @return {Audio} A reference to this instance. */ setFilter(filter) { return this.setFilters(filter ? [filter] : []); } /** * Sets the playback rate. * * Can only be used with compatible audio sources that allow playback control. * * @param {number} [value] - The playback rate to set. * @return {Audio|undefined} A reference to this instance. */ setPlaybackRate(value) { if (this.hasPlaybackControl === false) { console.warn('THREE.Audio: this Audio has no playback control.'); return; } this.playbackRate = value; if (this.isPlaying === true) { this.source.playbackRate.setTargetAtTime(this.playbackRate, this.context.currentTime, 0.01); } return this; } /** * Returns the current playback rate. * @return {number} The playback rate. */ getPlaybackRate() { return this.playbackRate; } /** * Automatically called when playback finished. */ onEnded() { this.isPlaying = false; this._progress = 0; } /** * Returns the loop flag. * * Can only be used with compatible audio sources that allow playback control. * * @return {boolean} Whether the audio should loop or not. */ getLoop() { if (this.hasPlaybackControl === false) { console.warn('THREE.Audio: this Audio has no playback control.'); return false; } return this.loop; } /** * Sets the loop flag. * * Can only be used with compatible audio sources that allow playback control. * * @param {boolean} value - Whether the audio should loop or not. * @return {Audio|undefined} A reference to this instance. */ setLoop(value) { if (this.hasPlaybackControl === false) { console.warn('THREE.Audio: this Audio has no playback control.'); return; } this.loop = value; if (this.isPlaying === true) { this.source.loop = this.loop; } return this; } /** * Sets the loop start value which defines where in the audio buffer the replay should * start, in seconds. * * @param {number} value - The loop start value. * @return {Audio} A reference to this instance. */ setLoopStart(value) { this.loopStart = value; return this; } /** * Sets the loop end value which defines where in the audio buffer the replay should * stop, in seconds. * * @param {number} value - The loop end value. * @return {Audio} A reference to this instance. */ setLoopEnd(value) { this.loopEnd = value; return this; } /** * Returns the volume. * * @return {number} The volume. */ getVolume() { return this.gain.gain.value; } /** * Sets the volume. * * @param {number} value - The volume to set. * @return {Audio} A reference to this instance. */ setVolume(value) { this.gain.gain.setTargetAtTime(value, this.context.currentTime, 0.01); return this; } copy(source, recursive) { super.copy(source, recursive); if (source.sourceType !== 'buffer') { console.warn('THREE.Audio: Audio source type cannot be copied.'); return this; } this.autoplay = source.autoplay; this.buffer = source.buffer; this.detune = source.detune; this.loop = source.loop; this.loopStart = source.loopStart; this.loopEnd = source.loopEnd; this.offset = source.offset; this.duration = source.duration; this.playbackRate = source.playbackRate; this.hasPlaybackControl = source.hasPlaybackControl; this.sourceType = source.sourceType; this.filters = source.filters.slice(); return this; } clone(recursive) { return new this.constructor(this.listener).copy(this, recursive); } } export { Audio };