mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2025-01-03 11:35:49 -05:00
Merge pull request #98 from gnarf/soundbank-effectchain
Soundbank effectchain
This commit is contained in:
commit
a79684a720
10 changed files with 649 additions and 512 deletions
|
@ -5,9 +5,15 @@ const log = require('./log');
|
||||||
const uid = require('./uid');
|
const uid = require('./uid');
|
||||||
|
|
||||||
const ADPCMSoundDecoder = require('./ADPCMSoundDecoder');
|
const ADPCMSoundDecoder = require('./ADPCMSoundDecoder');
|
||||||
const AudioPlayer = require('./AudioPlayer');
|
|
||||||
const Loudness = require('./Loudness');
|
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
|
* Wrapper to ensure that audioContext.decodeAudioData is a promise
|
||||||
|
@ -63,6 +69,12 @@ class AudioEngine {
|
||||||
* @type {Loudness}
|
* @type {Loudness}
|
||||||
*/
|
*/
|
||||||
this.loudness = null;
|
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.
|
* Retrieve the audio buffer as held in memory for a given sound id.
|
||||||
* @param {!string} soundId - the id of the sound buffer to get
|
* @todo remove this
|
||||||
* @return {AudioBuffer} the buffer corresponding to the given sound id.
|
|
||||||
*/
|
*/
|
||||||
getSoundBuffer (soundId) {
|
getSoundBuffer () {
|
||||||
// todo: Deprecate audioBuffers. If something wants to hold onto the
|
// todo: Deprecate audioBuffers. If something wants to hold onto the
|
||||||
// buffer, it should. Otherwise buffers need to be able to release their
|
// buffer, it should. Otherwise buffers need to be able to release their
|
||||||
// decoded memory to avoid running out of memory which is possible with
|
// decoded memory to avoid running out of memory which is possible with
|
||||||
// enough large audio buffers as they are full 16bit pcm waveforms for
|
// enough large audio buffers as they are full 16bit pcm waveforms for
|
||||||
// each audio channel.
|
// 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.
|
* 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.
|
* @todo remove this
|
||||||
* @param {AudioBuffer} newBuffer - the new buffer to swap in.
|
|
||||||
* @return {string} The uid of the sound that was updated or added
|
|
||||||
*/
|
*/
|
||||||
updateSoundBuffer (soundId, newBuffer) {
|
updateSoundBuffer () {
|
||||||
this.audioBuffers[soundId] = newBuffer;
|
log.warn('The updateSoundBuffer function is no longer available. Use soundBank.getSoundPlayer().buffer.');
|
||||||
return soundId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -233,13 +241,21 @@ class AudioEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an AudioPlayer. Each sprite or clone has an AudioPlayer.
|
* Deprecated way to create an AudioPlayer
|
||||||
* It includes a reference to the AudioEngine so it can use global
|
* @todo remove this
|
||||||
* functionality such as playing notes.
|
|
||||||
* @return {AudioPlayer} new AudioPlayer instance
|
|
||||||
*/
|
*/
|
||||||
createPlayer () {
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -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
162
src/SoundBank.js
Normal 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;
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
constructor (audioContext) {
|
constructor (audioEngine, {id, buffer}) {
|
||||||
this.audioContext = audioContext;
|
super();
|
||||||
this.outputNode = null;
|
|
||||||
this.buffer = null;
|
|
||||||
this.bufferSource = null;
|
|
||||||
this.playbackRate = 1;
|
|
||||||
this.isPlaying = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.id = id;
|
||||||
* Connect the SoundPlayer to an output node
|
|
||||||
* @param {GainNode} node - an output node to connect to
|
|
||||||
*/
|
|
||||||
connect (node) {
|
|
||||||
this.outputNode = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.audioEngine = audioEngine;
|
||||||
* Set an audio buffer
|
|
||||||
* @param {AudioBuffer} buffer - Buffer to set
|
|
||||||
*/
|
|
||||||
setBuffer (buffer) {
|
|
||||||
this.buffer = buffer;
|
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
|
* Is plaback currently starting?
|
||||||
* @param {number} playbackRate - a ratio where 1 is normal playback, 0.5 is half speed, 2 is double speed, etc.
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
setPlaybackRate (playbackRate) {
|
get isStarting () {
|
||||||
this.playbackRate = playbackRate;
|
return this.isPlaying && this.startingUntil > this.audioEngine.audioContext.currentTime;
|
||||||
if (this.bufferSource && this.bufferSource.playbackRate) {
|
}
|
||||||
this.bufferSource.playbackRate.value = this.playbackRate;
|
|
||||||
|
/**
|
||||||
|
* 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 () {
|
onEnded () {
|
||||||
if (this.bufferSource && this.isPlaying) {
|
this.emit('stop');
|
||||||
this.bufferSource.stop();
|
|
||||||
}
|
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start playing the sound
|
* Create the buffer source node during initialization or secondary
|
||||||
* The web audio framework requires a new audio buffer source node for each playback
|
* playback.
|
||||||
*/
|
*/
|
||||||
start () {
|
_createSource () {
|
||||||
if (!this.buffer) {
|
if (this.outputNode !== null) {
|
||||||
log.warn('tried to play a sound that was not loaded yet');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bufferSource = this.audioContext.createBufferSource();
|
this.target = target;
|
||||||
this.bufferSource.buffer = this.buffer;
|
|
||||||
this.bufferSource.playbackRate.value = this.playbackRate;
|
|
||||||
this.bufferSource.connect(this.outputNode);
|
|
||||||
this.bufferSource.start();
|
|
||||||
|
|
||||||
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
|
* Teardown the player.
|
||||||
* has been changed
|
*/
|
||||||
* @return {Promise} a Promise that resolves when the sound finishes playing
|
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 () {
|
finished () {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.bufferSource.onended = () => {
|
this.once('stop', resolve);
|
||||||
this.isPlaying = false;
|
|
||||||
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;
|
module.exports = SoundPlayer;
|
||||||
|
|
|
@ -23,6 +23,14 @@ class Effect {
|
||||||
this.target = null;
|
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.
|
* Default value to set the Effect to when constructed and when clear'ed.
|
||||||
* @const {number}
|
* @const {number}
|
||||||
|
|
182
src/effects/EffectChain.js
Normal file
182
src/effects/EffectChain.js
Normal 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;
|
|
@ -20,6 +20,10 @@ class PanEffect extends Effect {
|
||||||
this.channelMerger = null;
|
this.channelMerger = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get name () {
|
||||||
|
return 'pan';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Effect.
|
* Initialize the Effect.
|
||||||
* Effects start out uninitialized. Then initialize when they are first set
|
* Effects start out uninitialized. Then initialize when they are first set
|
||||||
|
|
|
@ -35,6 +35,10 @@ class PitchEffect extends Effect {
|
||||||
this.ratio = 1;
|
this.ratio = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get name () {
|
||||||
|
return 'pitch';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the effect be connected to the audio graph?
|
* Should the effect be connected to the audio graph?
|
||||||
* @return {boolean} is the effect affecting the graph?
|
* @return {boolean} is the effect affecting the graph?
|
||||||
|
|
|
@ -12,6 +12,10 @@ class VolumeEffect extends Effect {
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get name () {
|
||||||
|
return 'volume';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Effect.
|
* Initialize the Effect.
|
||||||
* Effects start out uninitialized. Then initialize when they are first set
|
* Effects start out uninitialized. Then initialize when they are first set
|
||||||
|
|
Loading…
Reference in a new issue