Merge pull request #98 from gnarf/soundbank-effectchain

Soundbank effectchain
This commit is contained in:
Mx Corey Frang 2018-06-21 16:58:56 -04:00 committed by GitHub
commit a79684a720
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 649 additions and 512 deletions

View file

@ -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];
}
/**
@ -186,27 +198,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.');
}
/**
@ -233,13 +241,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());
}
}

View file

@ -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<string, SoundPlayer>} 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;

View file

@ -1,290 +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.volumeEffect = null;
this.target = null;
this.initialized = false;
this.isPlaying = false;
this.startingUntil = 0;
this.playbackRate = 1;
// handleEvent is a EventTarget api for the DOM, however the web-audio-test-api we use
// uses an addEventListener that isn't compatable with object and requires us to pass
// this bound function instead
this.handleEvent = this.handleEvent.bind(this);
}
/**
* 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.handleEvent);
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.handleEvent);
if (this.target !== null) {
this.connect(this.target);
}
}
/**
* Initialize the player for first playback.
*/
initialize () {
this.initialized = true;
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;
}
if (this.volumeEffect === null) {
this.outputNode.disconnect();
this.outputNode.connect(target.getInputNode());
} else {
this.volumeEffect.connect(target);
}
return this;
}
/**
* Teardown the player.
*/
dispose () {
if (!this.initialized) {
return;
}
this.stopImmediately();
if (this.volumeEffect !== null) {
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.handleEvent);
}
const taken = new SoundPlayer(this.audioEngine, this);
taken.playbackRate = this.playbackRate;
if (this.isPlaying) {
taken.startingUntil = this.startingUntil;
taken.isPlaying = this.isPlaying;
taken.initialized = this.initialized;
taken.outputNode = this.outputNode;
taken.outputNode.addEventListener(ON_ENDED, taken.handleEvent);
taken.volumeEffect = this.volumeEffect;
if (taken.volumeEffect) {
taken.volumeEffect.audioPlayer = taken;
}
if (this.target !== null) {
taken.connect(this.target);
}
this.emit('stop');
taken.emit('play');
}
this.outputNode = null;
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) {
this.emit('stop');
this.emit('play');
return;
}
if (this.isPlaying) {
this.stop();
}
if (this.initialized) {
this._createSource();
} else {
this.initialize();
}
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;
}
// always do a manual stop on a taken / volume effect fade out sound player
// take will emit "stop" as well as reset all of our playing statuses / remove our
// nodes / etc
const taken = this.take();
taken.volumeEffect = new VolumeEffect(taken.audioEngine, taken, null);
taken.volumeEffect.connect(taken.target);
// volumeEffect will recursively connect to us if it needs to, so this happens too:
// taken.connect(taken.volumeEffect);
taken.finished().then(() => taken.dispose());
taken.volumeEffect.set(0);
taken.outputNode.stop(this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME);
}
/**
* 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;

162
src/SoundBank.js Normal file
View file

@ -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<SoundPlayer>}
*/
this.soundPlayers = {};
/**
* Map of targets by sound id.
* @type {Map<string, Target>}
*/
this.playerTargets = new Map();
/**
* Map of effect chains by sound id.
* @type {Map<string, EffectChain}
*/
this.soundEffects = new Map();
/**
* Original EffectChain cloned for every playing sound.
* @type {EffectChain}
*/
this.effectChainPrime = effectChainPrime;
}
/**
* Add a sound player instance likely from AudioEngine.decodeSoundPlayer
* @param {SoundPlayer} soundPlayer - SoundPlayer to add
*/
addSoundPlayer (soundPlayer) {
this.soundPlayers[soundPlayer.id] = soundPlayer;
}
/**
* Get a sound player by id.
* @param {string} soundId - sound to look for
* @returns {SoundPlayer} instance of sound player for the id
*/
getSoundPlayer (soundId) {
if (!this.soundPlayers[soundId]) {
log.error(`SoundBank.getSoundPlayer(${soundId}): called missing sound in bank`);
}
return this.soundPlayers[soundId];
}
/**
* Get a sound EffectChain by id.
* @param {string} sound - sound to look for an EffectChain
* @returns {EffectChain} available EffectChain for this id
*/
getSoundEffects (sound) {
if (!this.soundEffects.has(sound)) {
this.soundEffects.set(sound, this.effectChainPrime.clone());
}
return this.soundEffects.get(sound);
}
/**
* Play a sound.
* @param {Target} target - Target to play for
* @param {string} soundId - id of sound to play
* @returns {Promise} promise that resolves when the sound finishes playback
*/
playSound (target, soundId) {
const effects = this.getSoundEffects(soundId);
const player = this.getSoundPlayer(soundId);
if (this.playerTargets.get(soundId) !== target) {
// make sure to stop the old sound, effectively "forking" the output
// when the target switches before we adjust it's effects
player.stop();
}
this.playerTargets.set(soundId, target);
effects.addSoundPlayer(player);
effects.setEffectsFromTarget(target);
player.connect(effects);
player.play();
return player.finished();
}
/**
* Set the effects (pan, pitch, and volume) from values on the given target.
* @param {Target} target - target to set values from
*/
setEffects (target) {
this.playerTargets.forEach((playerTarget, key) => {
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;

View file

@ -1,91 +1,290 @@
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;
this.outputNode = null;
this.buffer = null;
this.bufferSource = null;
this.playbackRate = 1;
this.isPlaying = false;
}
constructor (audioEngine, {id, buffer}) {
super();
/**
* Connect the SoundPlayer to an output node
* @param {GainNode} node - an output node to connect to
*/
connect (node) {
this.outputNode = node;
}
this.id = id;
/**
* Set an audio buffer
* @param {AudioBuffer} buffer - Buffer to set
*/
setBuffer (buffer) {
this.audioEngine = audioEngine;
this.buffer = buffer;
this.outputNode = null;
this.volumeEffect = null;
this.target = null;
this.initialized = false;
this.isPlaying = false;
this.startingUntil = 0;
this.playbackRate = 1;
// handleEvent is a EventTarget api for the DOM, however the web-audio-test-api we use
// uses an addEventListener that isn't compatable with object and requires us to pass
// this bound function instead
this.handleEvent = this.handleEvent.bind(this);
}
/**
* 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.
* Is plaback currently starting?
* @type {boolean}
*/
setPlaybackRate (playbackRate) {
this.playbackRate = playbackRate;
if (this.bufferSource && this.bufferSource.playbackRate) {
this.bufferSource.playbackRate.value = this.playbackRate;
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();
}
}
/**
* Stop the sound
* Event listener for when playback ends.
*/
stop () {
if (this.bufferSource && this.isPlaying) {
this.bufferSource.stop();
}
onEnded () {
this.emit('stop');
this.isPlaying = false;
}
/**
* Start playing the sound
* The web audio framework requires a new audio buffer source node for each playback
* Create the buffer source node during initialization or secondary
* playback.
*/
start () {
if (!this.buffer) {
log.warn('tried to play a sound that was not loaded yet');
_createSource () {
if (this.outputNode !== null) {
this.outputNode.removeEventListener(ON_ENDED, this.handleEvent);
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.handleEvent);
if (this.target !== null) {
this.connect(this.target);
}
}
/**
* Initialize the player for first playback.
*/
initialize () {
this.initialized = true;
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.bufferSource = this.audioContext.createBufferSource();
this.bufferSource.buffer = this.buffer;
this.bufferSource.playbackRate.value = this.playbackRate;
this.bufferSource.connect(this.outputNode);
this.bufferSource.start();
this.target = target;
this.isPlaying = true;
if (!this.initialized) {
return;
}
if (this.volumeEffect === null) {
this.outputNode.disconnect();
this.outputNode.connect(target.getInputNode());
} else {
this.volumeEffect.connect(target);
}
return this;
}
/**
* 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
* Teardown the player.
*/
dispose () {
if (!this.initialized) {
return;
}
this.stopImmediately();
if (this.volumeEffect !== null) {
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.handleEvent);
}
const taken = new SoundPlayer(this.audioEngine, this);
taken.playbackRate = this.playbackRate;
if (this.isPlaying) {
taken.startingUntil = this.startingUntil;
taken.isPlaying = this.isPlaying;
taken.initialized = this.initialized;
taken.outputNode = this.outputNode;
taken.outputNode.addEventListener(ON_ENDED, taken.handleEvent);
taken.volumeEffect = this.volumeEffect;
if (taken.volumeEffect) {
taken.volumeEffect.audioPlayer = taken;
}
if (this.target !== null) {
taken.connect(this.target);
}
this.emit('stop');
taken.emit('play');
}
this.outputNode = null;
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) {
this.emit('stop');
this.emit('play');
return;
}
if (this.isPlaying) {
this.stop();
}
if (this.initialized) {
this._createSource();
} else {
this.initialize();
}
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;
}
// always do a manual stop on a taken / volume effect fade out sound player
// take will emit "stop" as well as reset all of our playing statuses / remove our
// nodes / etc
const taken = this.take();
taken.volumeEffect = new VolumeEffect(taken.audioEngine, taken, null);
taken.volumeEffect.connect(taken.target);
// volumeEffect will recursively connect to us if it needs to, so this happens too:
// taken.connect(taken.volumeEffect);
taken.finished().then(() => taken.dispose());
taken.volumeEffect.set(0);
taken.outputNode.stop(this.audioEngine.audioContext.currentTime + this.audioEngine.DECAY_TIME);
}
/**
* 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;

View file

@ -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}

182
src/effects/EffectChain.js Normal file
View file

@ -0,0 +1,182 @@
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<Effect>} 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<Effect>}
*/
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<Effect>}
*/
this._effects = effects
.reverse()
.map(Effect => {
const effect = new Effect(audioEngine, this, lastEffect);
this[effect.name] = effect;
lastEffect = effect;
return effect;
})
.reverse();
/**
* 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 === lastEffect) {
this.inputNode.disconnect();
this.inputNode.connect(lastEffect.getInputNode());
return;
} else if (target === firstEffect) {
return;
}
this.target = target;
firstEffect.connect(target);
}
/**
* Array of SoundPlayers managed by this EffectChain.
* @returns {Array<SoundPlayer>} 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;

View file

@ -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

View file

@ -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?

View file

@ -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