First Draft SoundBank + EffectChain

This commit is contained in:
Corey Frang 2018-06-15 10:28:02 -04:00
parent 444aba7f76
commit f5c219ceb3
7 changed files with 205 additions and 0 deletions

View file

@ -9,6 +9,12 @@ const AudioPlayer = require('./AudioPlayer');
const Loudness = require('./Loudness'); const Loudness = require('./Loudness');
const SoundPlayer = require('./GreenPlayer'); 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 * Wrapper to ensure that audioContext.decodeAudioData is a promise
* @param {object} audioContext The current AudioContext * @param {object} audioContext The current AudioContext
@ -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];
} }
/** /**
@ -241,6 +253,11 @@ class AudioEngine {
createPlayer () { createPlayer () {
return new AudioPlayer(this); return new AudioPlayer(this);
} }
createBank () {
return new SoundBank(this);
}
} }
module.exports = AudioEngine; module.exports = AudioEngine;

84
src/SoundBank.js Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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