diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 8c7e142..339eb8f 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -5,9 +5,15 @@ const log = require('./log'); const uid = require('./uid'); const ADPCMSoundDecoder = require('./ADPCMSoundDecoder'); -const AudioPlayer = require('./AudioPlayer'); const Loudness = require('./Loudness'); -const SoundPlayer = require('./GreenPlayer'); +const SoundPlayer = require('./SoundPlayer'); + +const EffectChain = require('./effects/EffectChain'); +const PanEffect = require('./effects/PanEffect'); +const PitchEffect = require('./effects/PitchEffect'); +const VolumeEffect = require('./effects/VolumeEffect'); + +const SoundBank = require('./SoundBank'); /** * Wrapper to ensure that audioContext.decodeAudioData is a promise @@ -63,6 +69,12 @@ class AudioEngine { * @type {Loudness} */ this.loudness = null; + + /** + * Array of effects applied in order, left to right, + * Left is closest to input, Right is closest to output + */ + this.effects = [PanEffect, PitchEffect, VolumeEffect]; } /** @@ -178,27 +190,23 @@ class AudioEngine { /** * Retrieve the audio buffer as held in memory for a given sound id. - * @param {!string} soundId - the id of the sound buffer to get - * @return {AudioBuffer} the buffer corresponding to the given sound id. + * @todo remove this */ - getSoundBuffer (soundId) { + getSoundBuffer () { // todo: Deprecate audioBuffers. If something wants to hold onto the // buffer, it should. Otherwise buffers need to be able to release their // decoded memory to avoid running out of memory which is possible with // enough large audio buffers as they are full 16bit pcm waveforms for // each audio channel. - return this.audioBuffers[soundId]; + log.warn('The getSoundBuffer function is no longer available. Use soundBank.getSoundPlayer().buffer.'); } /** * Add or update the in-memory audio buffer to a new one by soundId. - * @param {!string} soundId - the id of the sound buffer to update. - * @param {AudioBuffer} newBuffer - the new buffer to swap in. - * @return {string} The uid of the sound that was updated or added + * @todo remove this */ - updateSoundBuffer (soundId, newBuffer) { - this.audioBuffers[soundId] = newBuffer; - return soundId; + updateSoundBuffer () { + log.warn('The updateSoundBuffer function is no longer available. Use soundBank.getSoundPlayer().buffer.'); } /** @@ -225,13 +233,21 @@ class AudioEngine { } /** - * Create an AudioPlayer. Each sprite or clone has an AudioPlayer. - * It includes a reference to the AudioEngine so it can use global - * functionality such as playing notes. - * @return {AudioPlayer} new AudioPlayer instance + * Deprecated way to create an AudioPlayer + * @todo remove this */ createPlayer () { - return new AudioPlayer(this); + log.warn('the createPlayer method is no longer available, please use createBank'); + } + + createEffectChain () { + const effects = new EffectChain(this, this.effects); + effects.connect(this); + return effects; + } + + createBank () { + return new SoundBank(this, this.createEffectChain()); } } diff --git a/src/AudioPlayer.js b/src/AudioPlayer.js deleted file mode 100644 index e6513c8..0000000 --- a/src/AudioPlayer.js +++ /dev/null @@ -1,152 +0,0 @@ -const PanEffect = require('./effects/PanEffect'); -const PitchEffect = require('./effects/PitchEffect'); -const VolumeEffect = require('./effects/VolumeEffect'); - -const SoundPlayer = require('./GreenPlayer'); - -class AudioPlayer { - /** - * Each sprite or clone has an audio player - * the audio player handles sound playback, volume, and the sprite-specific audio effects: - * pitch and pan - * @param {AudioEngine} audioEngine AudioEngine for player - * @constructor - */ - constructor (audioEngine) { - this.audioEngine = audioEngine; - - this.outputNode = this.audioEngine.audioContext.createGain(); - - // Create the audio effects. - const volumeEffect = new VolumeEffect(this.audioEngine, this, null); - const pitchEffect = new PitchEffect(this.audioEngine, this, volumeEffect); - const panEffect = new PanEffect(this.audioEngine, this, pitchEffect); - this.effects = { - volume: volumeEffect, - pitch: pitchEffect, - pan: panEffect - }; - - // Chain the effects and player together with the audio engine. - // outputNode -> "pitchEffect" -> panEffect -> audioEngine.input - panEffect.connect(this.audioEngine); - pitchEffect.connect(panEffect); - volumeEffect.connect(pitchEffect); - - // Reset effects to their default parameters. - this.clearEffects(); - - // SoundPlayers mapped by sound id. - this.soundPlayers = {}; - } - - /** - * Get this sprite's input node, so that other objects can route sound through it. - * @return {AudioNode} the AudioNode for this sprite's input - */ - getInputNode () { - return this.outputNode; - } - - /** - * Get all the sound players owned by this audio player. - * @return {object} mapping of sound ids to sound - * players - */ - getSoundPlayers () { - return this.soundPlayers; - } - - /** - * Add a SoundPlayer instance to soundPlayers map. - * @param {SoundPlayer} soundPlayer - SoundPlayer instance to add - */ - addSoundPlayer (soundPlayer) { - this.soundPlayers[soundPlayer.id] = soundPlayer; - - for (const effectName in this.effects) { - this.effects[effectName].update(); - } - } - - /** - * Play a sound - * @param {string} soundId - the soundId id of a sound file - * @return {Promise} a Promise that resolves when the sound finishes playing - */ - playSound (soundId) { - // create a new soundplayer to play the sound - if (!this.soundPlayers[soundId]) { - this.addSoundPlayer(new SoundPlayer( - this.audioEngine, - {id: soundId, buffer: this.audioEngine.audioBuffers[soundId]} - )); - } - const player = this.soundPlayers[soundId]; - player.connect(this); - player.play(); - - return player.finished(); - } - - /** - * Stop all sounds that are playing - */ - stopAllSounds () { - // stop all active sound players - for (const soundId in this.soundPlayers) { - this.soundPlayers[soundId].stop(); - } - } - - /** - * Set an audio effect to a value - * @param {string} effect - the name of the effect - * @param {number} value - the value to set the effect to - */ - setEffect (effect, value) { - if (this.effects.hasOwnProperty(effect)) { - this.effects[effect].set(value); - } - } - - /** - * Clear all audio effects - */ - clearEffects () { - for (const effectName in this.effects) { - this.effects[effectName].clear(); - } - } - - /** - * Set the volume for sounds played by this AudioPlayer - * @param {number} value - the volume in range 0-100 - */ - setVolume (value) { - this.setEffect('volume', value); - } - - /** - * Connnect this player's output to another audio node - * @param {object} target - target whose node to should be connected - */ - connect (target) { - this.outputNode.disconnect(); - this.outputNode.connect(target.getInputNode()); - } - - /** - * Clean up and disconnect audio nodes. - */ - dispose () { - this.effects.volume.dispose(); - this.effects.pitch.dispose(); - this.effects.pan.dispose(); - - this.outputNode.disconnect(); - this.outputNode = null; - } -} - -module.exports = AudioPlayer; diff --git a/src/GreenPlayer.js b/src/GreenPlayer.js deleted file mode 100644 index e73954a..0000000 --- a/src/GreenPlayer.js +++ /dev/null @@ -1,277 +0,0 @@ -const {EventEmitter} = require('events'); - -const VolumeEffect = require('./effects/VolumeEffect'); - -/** - * Name of event that indicates playback has ended. - * @const {string} - */ -const ON_ENDED = 'ended'; - -class SoundPlayer extends EventEmitter { - /** - * Play sounds that stop without audible clipping. - * - * @param {AudioEngine} audioEngine - engine to play sounds on - * @param {object} data - required data for sound playback - * @param {string} data.id - a unique id for this sound - * @param {ArrayBuffer} data.buffer - buffer of the sound's waveform to play - * @constructor - */ - constructor (audioEngine, {id, buffer}) { - super(); - - this.id = id; - - this.audioEngine = audioEngine; - this.buffer = buffer; - - this.outputNode = null; - this.target = null; - - this.initialized = false; - this.isPlaying = false; - this.startingUntil = 0; - this.playbackRate = 1; - } - - /** - * Is plaback currently starting? - * @type {boolean} - */ - get isStarting () { - return this.isPlaying && this.startingUntil > this.audioEngine.audioContext.currentTime; - } - - /** - * Handle any event we have told the output node to listen for. - * @param {Event} event - dom event to handle - */ - handleEvent (event) { - if (event.type === ON_ENDED) { - this.onEnded(); - } - } - - /** - * Event listener for when playback ends. - */ - onEnded () { - this.emit('stop'); - - this.isPlaying = false; - } - - /** - * Create the buffer source node during initialization or secondary - * playback. - */ - _createSource () { - if (this.outputNode !== null) { - this.outputNode.removeEventListener(ON_ENDED, this); - this.outputNode.disconnect(); - } - - this.outputNode = this.audioEngine.audioContext.createBufferSource(); - this.outputNode.playbackRate.value = this.playbackRate; - this.outputNode.buffer = this.buffer; - - this.outputNode.addEventListener(ON_ENDED, this); - - if (this.target !== null) { - this.connect(this.target); - } - } - - /** - * Initialize the player for first playback. - */ - initialize () { - this.initialized = true; - - this.volumeEffect = new VolumeEffect(this.audioEngine, this, null); - - this._createSource(); - } - - /** - * Connect the player to the engine or an effect chain. - * @param {object} target - object to connect to - * @returns {object} - return this sound player - */ - connect (target) { - if (target === this.volumeEffect) { - this.outputNode.disconnect(); - this.outputNode.connect(this.volumeEffect.getInputNode()); - return; - } - - this.target = target; - - if (!this.initialized) { - return; - } - - this.volumeEffect.connect(target); - - return this; - } - - /** - * Teardown the player. - */ - dispose () { - if (!this.initialized) { - return; - } - - this.stopImmediately(); - - this.volumeEffect.dispose(); - this.volumeEffect = null; - - this.outputNode.disconnect(); - this.outputNode = null; - - this.target = null; - - this.initialized = false; - } - - /** - * Take the internal state of this player and create a new player from - * that. Restore the state of this player to that before its first playback. - * - * The returned player can be used to stop the original playback or - * continue it without manipulation from the original player. - * - * @returns {SoundPlayer} - new SoundPlayer with old state - */ - take () { - if (this.outputNode) { - this.outputNode.removeEventListener(ON_ENDED, this); - } - - const taken = new SoundPlayer(this.audioEngine, this); - taken.playbackRate = this.playbackRate; - if (this.isPlaying) { - taken.startingUntil = this.startingUntil; - taken.isPlaying = this.isPlaying; - taken.initialize(); - taken.outputNode.disconnect(); - taken.outputNode = this.outputNode; - taken.outputNode.addEventListener(ON_ENDED, taken); - taken.volumeEffect.set(this.volumeEffect.value); - if (this.target !== null) { - taken.connect(this.target); - } - } - - if (this.isPlaying) { - this.emit('stop'); - taken.emit('play'); - } - - this.outputNode = null; - if (this.volumeEffect !== null) { - this.volumeEffect.dispose(); - } - this.volumeEffect = null; - this.initialized = false; - this.startingUntil = 0; - this.isPlaying = false; - - return taken; - } - - /** - * Start playback for this sound. - * - * If the sound is already playing it will stop playback with a quick fade - * out. - */ - play () { - if (this.isStarting) { - return; - } - - if (this.isPlaying) { - // Spawn a Player with the current buffer source, and play for a - // short period until its volume is 0 and release it to be - // eventually garbage collected. - this.take().stop(); - } - - if (!this.initialized) { - this.initialize(); - } else { - this._createSource(); - } - - this.volumeEffect.set(this.volumeEffect.DEFAULT_VALUE); - this.outputNode.start(); - - this.isPlaying = true; - - this.startingUntil = this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME; - - this.emit('play'); - } - - /** - * Stop playback after quickly fading out. - */ - stop () { - if (!this.isPlaying) { - return; - } - - this.volumeEffect.set(0); - this.outputNode.stop(this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME); - - this.isPlaying = false; - this.startingUntil = 0; - - this.emit('stop'); - } - - /** - * Stop immediately without fading out. May cause audible clipping. - */ - stopImmediately () { - if (!this.isPlaying) { - return; - } - - this.outputNode.stop(); - - this.isPlaying = false; - this.startingUntil = 0; - - this.emit('stop'); - } - - /** - * Return a promise that resolves when the sound next finishes. - * @returns {Promise} - resolves when the sound finishes - */ - finished () { - return new Promise(resolve => { - this.once('stop', resolve); - }); - } - - /** - * Set the sound's playback rate. - * @param {number} value - playback rate. Default is 1. - */ - setPlaybackRate (value) { - this.playbackRate = value; - - if (this.initialized) { - this.outputNode.playbackRate.value = value; - } - } -} - -module.exports = SoundPlayer; diff --git a/src/SoundBank.js b/src/SoundBank.js new file mode 100644 index 0000000..98ef0c4 --- /dev/null +++ b/src/SoundBank.js @@ -0,0 +1,162 @@ +const log = require('./log'); + +/** + * A symbol indicating all targets are to be effected. + * @const {string} + */ +const ALL_TARGETS = '*'; + +class SoundBank { + /** + * A bank of sounds that can be played. + * @constructor + * @param {AudioEngine} audioEngine - related AudioEngine + * @param {EffectChain} effectChainPrime - original EffectChain cloned for + * playing sounds + */ + constructor (audioEngine, effectChainPrime) { + /** + * AudioEngine this SoundBank is related to. + * @type {AudioEngine} + */ + this.audioEngine = audioEngine; + + /** + * Map of ids to soundPlayers. + * @type {object} + */ + this.soundPlayers = {}; + + /** + * Map of targets by sound id. + * @type {Map} + */ + this.playerTargets = new Map(); + + /** + * Map of effect chains by sound id. + * @type {Map { + if (playerTarget === target) { + this.getSoundEffects(key).setEffectsFromTarget(target); + } + }); + } + + /** + * Stop playback of sound by id if was lasted played by the target. + * @param {Target} target - target to check if it last played the sound + * @param {string} soundId - id of the sound to stop + */ + stop (target, soundId) { + if (this.playerTargets.get(soundId) === target) { + this.soundPlayers[soundId].stop(); + } + } + + /** + * Stop all sounds for all targets or a specific target. + * @param {Target|string} target - a symbol for all targets or the target + * to stop sounds for + */ + stopAllSounds (target = ALL_TARGETS) { + this.playerTargets.forEach((playerTarget, key) => { + if (target === ALL_TARGETS || playerTarget === target) { + this.getSoundPlayer(key).stop(); + } + }); + } + + /** + * Dispose of all EffectChains and SoundPlayers. + */ + dispose () { + this.playerTargets.clear(); + this.soundEffects.forEach(effects => effects.dispose()); + this.soundEffects.clear(); + for (const soundId in this.soundPlayers) { + if (this.soundPlayers.hasOwnProperty(soundId)) { + this.soundPlayers[soundId].dispose(); + } + } + this.soundPlayers = {}; + } + +} + +module.exports = SoundBank; diff --git a/src/SoundPlayer.js b/src/SoundPlayer.js index 009b3d5..e73954a 100644 --- a/src/SoundPlayer.js +++ b/src/SoundPlayer.js @@ -1,91 +1,277 @@ -const log = require('./log'); +const {EventEmitter} = require('events'); + +const VolumeEffect = require('./effects/VolumeEffect'); /** - * A SoundPlayer stores an audio buffer, and plays it + * Name of event that indicates playback has ended. + * @const {string} */ -class SoundPlayer { +const ON_ENDED = 'ended'; + +class SoundPlayer extends EventEmitter { /** - * @param {AudioContext} audioContext - a webAudio context + * Play sounds that stop without audible clipping. + * + * @param {AudioEngine} audioEngine - engine to play sounds on + * @param {object} data - required data for sound playback + * @param {string} data.id - a unique id for this sound + * @param {ArrayBuffer} data.buffer - buffer of the sound's waveform to play * @constructor */ - constructor (audioContext) { - this.audioContext = audioContext; + constructor (audioEngine, {id, buffer}) { + super(); + + this.id = id; + + this.audioEngine = audioEngine; + this.buffer = buffer; + this.outputNode = null; - this.buffer = null; - this.bufferSource = null; + this.target = null; + + this.initialized = false; + this.isPlaying = false; + this.startingUntil = 0; this.playbackRate = 1; + } + + /** + * Is plaback currently starting? + * @type {boolean} + */ + get isStarting () { + return this.isPlaying && this.startingUntil > this.audioEngine.audioContext.currentTime; + } + + /** + * Handle any event we have told the output node to listen for. + * @param {Event} event - dom event to handle + */ + handleEvent (event) { + if (event.type === ON_ENDED) { + this.onEnded(); + } + } + + /** + * Event listener for when playback ends. + */ + onEnded () { + this.emit('stop'); + this.isPlaying = false; } /** - * Connect the SoundPlayer to an output node - * @param {GainNode} node - an output node to connect to + * Create the buffer source node during initialization or secondary + * playback. */ - connect (node) { - this.outputNode = node; + _createSource () { + if (this.outputNode !== null) { + this.outputNode.removeEventListener(ON_ENDED, this); + this.outputNode.disconnect(); + } + + this.outputNode = this.audioEngine.audioContext.createBufferSource(); + this.outputNode.playbackRate.value = this.playbackRate; + this.outputNode.buffer = this.buffer; + + this.outputNode.addEventListener(ON_ENDED, this); + + if (this.target !== null) { + this.connect(this.target); + } } /** - * Set an audio buffer - * @param {AudioBuffer} buffer - Buffer to set + * Initialize the player for first playback. */ - setBuffer (buffer) { - this.buffer = buffer; + initialize () { + this.initialized = true; + + this.volumeEffect = new VolumeEffect(this.audioEngine, this, null); + + this._createSource(); } /** - * Set the playback rate for the sound - * @param {number} playbackRate - a ratio where 1 is normal playback, 0.5 is half speed, 2 is double speed, etc. + * Connect the player to the engine or an effect chain. + * @param {object} target - object to connect to + * @returns {object} - return this sound player */ - setPlaybackRate (playbackRate) { - this.playbackRate = playbackRate; - if (this.bufferSource && this.bufferSource.playbackRate) { - this.bufferSource.playbackRate.value = this.playbackRate; + connect (target) { + if (target === this.volumeEffect) { + this.outputNode.disconnect(); + this.outputNode.connect(this.volumeEffect.getInputNode()); + return; + } + + this.target = target; + + if (!this.initialized) { + return; } + + this.volumeEffect.connect(target); + + return this; } /** - * Stop the sound + * Teardown the player. */ - stop () { - if (this.bufferSource && this.isPlaying) { - this.bufferSource.stop(); + dispose () { + if (!this.initialized) { + return; } + + this.stopImmediately(); + + this.volumeEffect.dispose(); + this.volumeEffect = null; + + this.outputNode.disconnect(); + this.outputNode = null; + + this.target = null; + + this.initialized = false; + } + + /** + * Take the internal state of this player and create a new player from + * that. Restore the state of this player to that before its first playback. + * + * The returned player can be used to stop the original playback or + * continue it without manipulation from the original player. + * + * @returns {SoundPlayer} - new SoundPlayer with old state + */ + take () { + if (this.outputNode) { + this.outputNode.removeEventListener(ON_ENDED, this); + } + + const taken = new SoundPlayer(this.audioEngine, this); + taken.playbackRate = this.playbackRate; + if (this.isPlaying) { + taken.startingUntil = this.startingUntil; + taken.isPlaying = this.isPlaying; + taken.initialize(); + taken.outputNode.disconnect(); + taken.outputNode = this.outputNode; + taken.outputNode.addEventListener(ON_ENDED, taken); + taken.volumeEffect.set(this.volumeEffect.value); + if (this.target !== null) { + taken.connect(this.target); + } + } + + if (this.isPlaying) { + this.emit('stop'); + taken.emit('play'); + } + + this.outputNode = null; + if (this.volumeEffect !== null) { + this.volumeEffect.dispose(); + } + this.volumeEffect = null; + this.initialized = false; + this.startingUntil = 0; this.isPlaying = false; + + return taken; } /** - * Start playing the sound - * The web audio framework requires a new audio buffer source node for each playback + * Start playback for this sound. + * + * If the sound is already playing it will stop playback with a quick fade + * out. */ - start () { - if (!this.buffer) { - log.warn('tried to play a sound that was not loaded yet'); + play () { + if (this.isStarting) { return; } - this.bufferSource = this.audioContext.createBufferSource(); - this.bufferSource.buffer = this.buffer; - this.bufferSource.playbackRate.value = this.playbackRate; - this.bufferSource.connect(this.outputNode); - this.bufferSource.start(); + if (this.isPlaying) { + // Spawn a Player with the current buffer source, and play for a + // short period until its volume is 0 and release it to be + // eventually garbage collected. + this.take().stop(); + } + + if (!this.initialized) { + this.initialize(); + } else { + this._createSource(); + } + + this.volumeEffect.set(this.volumeEffect.DEFAULT_VALUE); + this.outputNode.start(); this.isPlaying = true; + + this.startingUntil = this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME; + + this.emit('play'); } /** - * The sound has finished playing. This is called at the correct time even if the playback rate - * has been changed - * @return {Promise} a Promise that resolves when the sound finishes playing + * Stop playback after quickly fading out. + */ + stop () { + if (!this.isPlaying) { + return; + } + + this.volumeEffect.set(0); + this.outputNode.stop(this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME); + + this.isPlaying = false; + this.startingUntil = 0; + + this.emit('stop'); + } + + /** + * Stop immediately without fading out. May cause audible clipping. + */ + stopImmediately () { + if (!this.isPlaying) { + return; + } + + this.outputNode.stop(); + + this.isPlaying = false; + this.startingUntil = 0; + + this.emit('stop'); + } + + /** + * Return a promise that resolves when the sound next finishes. + * @returns {Promise} - resolves when the sound finishes */ finished () { return new Promise(resolve => { - this.bufferSource.onended = () => { - this.isPlaying = false; - resolve(); - }; + this.once('stop', resolve); }); } + + /** + * Set the sound's playback rate. + * @param {number} value - playback rate. Default is 1. + */ + setPlaybackRate (value) { + this.playbackRate = value; + + if (this.initialized) { + this.outputNode.playbackRate.value = value; + } + } } module.exports = SoundPlayer; diff --git a/src/effects/Effect.js b/src/effects/Effect.js index 8a4da12..fa87103 100644 --- a/src/effects/Effect.js +++ b/src/effects/Effect.js @@ -23,6 +23,14 @@ class Effect { this.target = null; } + /** + * Return the name of the effect. + * @type {string} + */ + get name () { + throw new Error(`${this.constructor.name}.name is not implemented`); + } + /** * Default value to set the Effect to when constructed and when clear'ed. * @const {number} diff --git a/src/effects/EffectChain.js b/src/effects/EffectChain.js new file mode 100644 index 0000000..4790fb7 --- /dev/null +++ b/src/effects/EffectChain.js @@ -0,0 +1,180 @@ +class EffectChain { + /** + * Chain of effects that can be applied to a group of SoundPlayers. + * @param {AudioEngine} audioEngine - engine whose effects these belong to + * @param {Array} effects - array of Effect classes to construct + */ + constructor (audioEngine, effects) { + /** + * AudioEngine whose effects these belong to. + * @type {AudioEngine} + */ + this.audioEngine = audioEngine; + + /** + * Node incoming connections will attach to. This node than connects to + * the items in the chain which finally connect to some output. + * @type {AudioNode} + */ + this.inputNode = this.audioEngine.audioContext.createGain(); + + /** + * List of Effect types to create. + * @type {Array} + */ + this.effects = effects; + + // Effects are instantiated in reverse so that the first refers to the + // second, the second refers to the third, etc and the last refers to + // null. + let lastEffect = null; + /** + * List of instantiated Effects. + * @type {Array} + */ + this._effects = effects + .map(Effect => { + const effect = new Effect(audioEngine, this, lastEffect); + this[effect.name] = effect; + lastEffect = effect; + return effect; + }); + + /** + * First effect of this chain. + * @type {Effect} + */ + this.firstEffect = this._effects[0]; + + /** + * Last effect of this chain. + * @type {Effect} + */ + this.lastEffect = this._effects[this._effects.length - 1]; + + /** + * A set of players this chain is managing. + */ + this._soundPlayers = new Set(); + } + + /** + * Create a clone of the EffectChain. + * @returns {EffectChain} a clone of this EffectChain + */ + clone () { + const chain = new EffectChain(this.audioEngine, this.effects); + if (this.target) { + chain.connect(this.target); + } + return chain; + } + + /** + * Add a sound player. + * @param {SoundPlayer} soundPlayer - a sound player to manage + */ + addSoundPlayer (soundPlayer) { + if (!this._soundPlayers.has(soundPlayer)) { + this._soundPlayers.add(soundPlayer); + this.update(); + } + } + + /** + * Remove a sound player. + * @param {SoundPlayer} soundPlayer - a sound player to stop managing + */ + removeSoundPlayer (soundPlayer) { + this._soundPlayers.remove(soundPlayer); + } + + /** + * Get the audio input node. + * @returns {AudioNode} audio node the upstream can connect to + */ + getInputNode () { + return this.inputNode; + } + + /** + * Connnect this player's output to another audio node. + * @param {object} target - target whose node to should be connected + */ + connect (target) { + const {firstEffect, lastEffect} = this; + + if (target === firstEffect) { + this.inputNode.disconnect(); + this.inputNode.connect(lastEffect.getInputNode()); + + return; + } else if (target === lastEffect) { + return; + } + + this.target = target; + + lastEffect.connect(target); + } + + /** + * Array of SoundPlayers managed by this EffectChain. + * @returns {Array} sound players managed by this chain + */ + getSoundPlayers () { + return [...this._soundPlayers]; + } + + /** + * Set Effect values with named values on target.soundEffects if it exist + * and then from target itself. + * @param {Target} target - target to set values from + */ + setEffectsFromTarget (target) { + this._effects.forEach(effect => { + if ('soundEffects' in target && effect.name in target.soundEffects) { + effect.set(target.soundEffects[effect.name]); + } else if (effect.name in target) { + effect.set(target[effect.name]); + } + }); + } + + /** + * Set an effect value by its name. + * @param {string} effect - effect name to change + * @param {number} value - value to set effect to + */ + set (effect, value) { + if (effect in this) { + this[effect].set(value); + } + } + + /** + * Update managed sound players with the effects on this chain. + */ + update () { + this._effects.forEach(effect => effect.update()); + } + + /** + * Clear all effects to their default values. + */ + clear () { + this._effects.forEach(effect => effect.clear()); + } + + /** + * Dispose of all effects in this chain. Nothing is done to managed + * SoundPlayers. + */ + dispose () { + this._soundPlayers = null; + this._effects.forEach(effect => effect.dispose()); + this._effects = null; + } +} + +module.exports = EffectChain; diff --git a/src/effects/PanEffect.js b/src/effects/PanEffect.js index d58558a..3255ee4 100644 --- a/src/effects/PanEffect.js +++ b/src/effects/PanEffect.js @@ -20,6 +20,10 @@ class PanEffect extends Effect { this.channelMerger = null; } + get name () { + return 'pan'; + } + /** * Initialize the Effect. * Effects start out uninitialized. Then initialize when they are first set diff --git a/src/effects/PitchEffect.js b/src/effects/PitchEffect.js index 2f71dd8..f4d2ba6 100644 --- a/src/effects/PitchEffect.js +++ b/src/effects/PitchEffect.js @@ -35,6 +35,10 @@ class PitchEffect extends Effect { this.ratio = 1; } + get name () { + return 'pitch'; + } + /** * Should the effect be connected to the audio graph? * @return {boolean} is the effect affecting the graph? diff --git a/src/effects/VolumeEffect.js b/src/effects/VolumeEffect.js index 32204f0..0713e41 100644 --- a/src/effects/VolumeEffect.js +++ b/src/effects/VolumeEffect.js @@ -12,6 +12,10 @@ class VolumeEffect extends Effect { return 100; } + get name () { + return 'volume'; + } + /** * Initialize the Effect. * Effects start out uninitialized. Then initialize when they are first set