three
Version:
JavaScript 3D library
778 lines (572 loc) • 15.3 kB
JavaScript
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 };