UNPKG

three

Version:

JavaScript 3D library

778 lines (572 loc) 15.3 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 };