From f5c219ceb34723c63ce3365f50cb0fb64a4e0924 Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Fri, 15 Jun 2018 10:28:02 -0400 Subject: [PATCH] First Draft SoundBank + EffectChain --- src/AudioEngine.js | 17 ++++++++ src/SoundBank.js | 84 +++++++++++++++++++++++++++++++++++++ src/effects/Effect.js | 8 ++++ src/effects/EffectChain.js | 84 +++++++++++++++++++++++++++++++++++++ src/effects/PanEffect.js | 4 ++ src/effects/PitchEffect.js | 4 ++ src/effects/VolumeEffect.js | 4 ++ 7 files changed, 205 insertions(+) create mode 100644 src/SoundBank.js create mode 100644 src/effects/EffectChain.js diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 7325f86..08e1b40 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -9,6 +9,12 @@ const AudioPlayer = require('./AudioPlayer'); const Loudness = require('./Loudness'); const SoundPlayer = require('./GreenPlayer'); +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 * @param {object} audioContext The current AudioContext @@ -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]; } /** @@ -241,6 +253,11 @@ class AudioEngine { createPlayer () { return new AudioPlayer(this); } + + + createBank () { + return new SoundBank(this); + } } module.exports = AudioEngine; diff --git a/src/SoundBank.js b/src/SoundBank.js new file mode 100644 index 0000000..d83b7d0 --- /dev/null +++ b/src/SoundBank.js @@ -0,0 +1,84 @@ +const SoundPlayer = require('./GreenPlayer'); +const EffectsChain = require('./effects/EffectChain'); + +const ALL_TARGETS = '*'; + +class SoundBank { + constructor (audioEngine) { + this.audioEngine = audioEngine; + + this.soundPlayers = {}; + this.playerTargets = new Map(); + this.soundEffects = new Map(); + } + + getSoundPlayer (soundId) { + if (!this.soundPlayers[soundId]) { + this.soundPlayers[soundId] = new SoundPlayer(this.audioEngine, { + id: soundId, buffer: this.audioEngine.audioBuffers[soundId] + }); + } + + return this.soundPlayers[soundId]; + } + + getSoundEffects (sound) { + if (!this.soundEffects.has(sound)) { + this.soundEffects.set(sound, new EffectsChain(this.audioEngine)); + } + + return this.soundEffects.get(sound); + } + + + playSound (target, soundId) { + const effects = this.getSoundEffects(soundId); + const player = this.getSoundPlayer(soundId); + + this.playerTargets.set(soundId, target); + effects.setEffectsFromTarget(target); + effects.addSoundPlayer(player); + + player.connect(effects); + player.play(); + + return player.finished(); + } + + setEffects (target) { + this.playerTargets.forEach((playerTarget, key) => { + if (playerTarget === target) { + this.getSoundEffects(key).setEffectsFromTarget(target); + } + }); + } + + stop (target, soundId) { + if (this.playerTargets.get(soundId) === target) { + this.soundPlayers[soundId].stop(); + } + } + + stopAllSounds (target = ALL_TARGETS) { + this.playerTargets.forEach((playerTarget, key) => { + if (target === ALL_TARGETS || playerTarget === target) { + this.getSoundPlayer(key).stop(); + } + }); + } + + 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/effects/Effect.js b/src/effects/Effect.js index 584e843..97ddee1 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..41c08d5 --- /dev/null +++ b/src/effects/EffectChain.js @@ -0,0 +1,84 @@ +class EffectChain { + constructor (audioEngine) { + this.audioEngine = audioEngine; + + this.outputNode = this.audioEngine.audioContext.createGain(); + + this.lastEffect = null; + + this._effects = audioEngine.effects.map(Effect => { + const effect = new Effect(audioEngine, this, this.lastEffect); + this[effect.name] = effect; + this.lastEffect = effect; + return effect; + }); + + // Walk backwards through effects connecting the last output to audio engine, + // then each effect's output to the input of the next effect. + this._effects.reduceRight((nextNode, effect) => { + effect.connect(nextNode); + return effect; + }, this.audioEngine); + + this._soundPlayers = new Set(); + } + + addSoundPlayer (soundPlayer) { + if (!this._soundPlayers.has(soundPlayer)) { + this._soundPlayers.add(soundPlayer); + this._effects.forEach(effect => effect.update()); + } + } + + removeSoundPlayer (soundPlayer) { + this._soundPlayers.remove(soundPlayer); + } + + getInputNode () { + return this.outputNode; + } + + /** + * 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()); + } + + + getSoundPlayers () { + return [...this._soundPlayers]; + } + + setEffectsFromTarget (target) { + this._effects.forEach(effect => { + if (effect.name in target) { + effect.set(target[effect.name]); + } else if ('soundEffects' in target && effect.name in target.soundEffects) { + effect.set(target.soundEffects[effect.name]); + } else { + effect.set(effect.DEFAULT_VALUE); + } + }); + } + + set (effect, value) { + if (effect in this) { + this[effect].set(value); + } + } + + clear () { + this._effects.forEach(effect => effect.clear()); + } + + 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 c5ec72e..1b9942a 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