From f5c219ceb34723c63ce3365f50cb0fb64a4e0924 Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Fri, 15 Jun 2018 10:28:02 -0400 Subject: [PATCH 1/9] 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 From d40564d61a6de36d6e1174de333cde4b0424657c Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 18 Jun 2018 11:59:45 -0400 Subject: [PATCH 2/9] make EffectChain standalone - EffectChain knows of audioEngine so it can create Effects - EffectChain connects to the target specified by .connect(target) - EffectChain can connect to other Effects or EffectChains - EffectChain can clone itself and connect to the original's target - Add EffectChain.update to mirror Effects API - Use deepest setting first in setEffectsFromTarget --- src/effects/EffectChain.js | 52 ++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/effects/EffectChain.js b/src/effects/EffectChain.js index 41c08d5..fa874ef 100644 --- a/src/effects/EffectChain.js +++ b/src/effects/EffectChain.js @@ -1,32 +1,35 @@ class EffectChain { - constructor (audioEngine) { + constructor (audioEngine, effects) { this.audioEngine = audioEngine; - this.outputNode = this.audioEngine.audioContext.createGain(); + this.inputNode = this.audioEngine.audioContext.createGain(); + + this.effects = effects; this.lastEffect = null; - this._effects = audioEngine.effects.map(Effect => { + this._effects = 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(); } + clone () { + const chain = new EffectChain(this.audioEngine, this.effects); + if (this.target === target) { + chain.connect(target); + } + return chain; + } + addSoundPlayer (soundPlayer) { if (!this._soundPlayers.has(soundPlayer)) { this._soundPlayers.add(soundPlayer); - this._effects.forEach(effect => effect.update()); + this.update(); } } @@ -35,7 +38,7 @@ class EffectChain { } getInputNode () { - return this.outputNode; + return this.inputNode; } /** @@ -43,8 +46,17 @@ class EffectChain { * @param {object} target - target whose node to should be connected */ connect (target) { - this.outputNode.disconnect(); - this.outputNode.connect(target.getInputNode()); + const {lastEffect} = this; + if (target === lastEffect) { + this.inputNode.disconnect(); + this.inputNode.connect(lastEffect.getInputNode()); + + return; + } + + this.target = target; + + this._effects[0].connect(target); } @@ -54,12 +66,10 @@ class EffectChain { 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) { + if ('soundEffects' in target && effect.name in target.soundEffects) { effect.set(target.soundEffects[effect.name]); - } else { - effect.set(effect.DEFAULT_VALUE); + } else if (effect.name in target) { + effect.set(target[effect.name]); } }); } @@ -70,6 +80,10 @@ class EffectChain { } } + update () { + this._effects.forEach(effect => effect.update()); + } + clear () { this._effects.forEach(effect => effect.clear()); } From 5177dd5c851edb30bf638518775d9b20f9d2b973 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 18 Jun 2018 12:03:20 -0400 Subject: [PATCH 3/9] add SoundPlayers to SoundBank - SoundBank may not create SoundPlayers (but it can call .take if need) - All SoundPlayers SoundBank plays are given to it --- src/SoundBank.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/SoundBank.js b/src/SoundBank.js index d83b7d0..ecbb1c4 100644 --- a/src/SoundBank.js +++ b/src/SoundBank.js @@ -1,22 +1,24 @@ -const SoundPlayer = require('./GreenPlayer'); -const EffectsChain = require('./effects/EffectChain'); +const log = require('./log'); const ALL_TARGETS = '*'; class SoundBank { - constructor (audioEngine) { + constructor (audioEngine, effectChainPrime) { this.audioEngine = audioEngine; this.soundPlayers = {}; this.playerTargets = new Map(); this.soundEffects = new Map(); + this.effectChainPrime = effectChainPrime; + } + + addSoundPlayer (soundPlayer) { + this.soundPlayers[soundPlayer.id] = soundPlayer; } getSoundPlayer (soundId) { if (!this.soundPlayers[soundId]) { - this.soundPlayers[soundId] = new SoundPlayer(this.audioEngine, { - id: soundId, buffer: this.audioEngine.audioBuffers[soundId] - }); + log.error(`SoundBank.getSoundPlayer(${soundId}): called missing sound in bank`); } return this.soundPlayers[soundId]; @@ -24,7 +26,7 @@ class SoundBank { getSoundEffects (sound) { if (!this.soundEffects.has(sound)) { - this.soundEffects.set(sound, new EffectsChain(this.audioEngine)); + this.soundEffects.set(sound, this.effectChainPrime.clone()); } return this.soundEffects.get(sound); From 7cef4e60a848c62546a983d046e0a84c24ab2d44 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 18 Jun 2018 12:10:14 -0400 Subject: [PATCH 4/9] pass main EffectChain to SoundBank from Engine.createBank --- src/AudioEngine.js | 21 ++++++++++----------- src/effects/EffectChain.js | 33 ++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 08e1b40..6781ef9 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -9,6 +9,7 @@ const AudioPlayer = require('./AudioPlayer'); const Loudness = require('./Loudness'); const SoundPlayer = require('./GreenPlayer'); +const EffectChain = require('./effects/EffectChain'); const PanEffect = require('./effects/PanEffect'); const PitchEffect = require('./effects/PitchEffect'); const VolumeEffect = require('./effects/VolumeEffect'); @@ -198,27 +199,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.'); } /** @@ -256,7 +253,9 @@ class AudioEngine { createBank () { - return new SoundBank(this); + const effects = new EffectChain(this, this.effects); + effects.connect(this); + return new SoundBank(this, effects); } } diff --git a/src/effects/EffectChain.js b/src/effects/EffectChain.js index fa874ef..e0b175f 100644 --- a/src/effects/EffectChain.js +++ b/src/effects/EffectChain.js @@ -6,22 +6,30 @@ class EffectChain { this.effects = effects; - this.lastEffect = null; + // Effects are instantiate 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; + this._effects = effects + .reverse() + .map(Effect => { + const effect = new Effect(audioEngine, this, lastEffect); + this[effect.name] = effect; + lastEffect = effect; + return effect; + }) + .reverse(); - this._effects = effects.map(Effect => { - const effect = new Effect(audioEngine, this, this.lastEffect); - this[effect.name] = effect; - this.lastEffect = effect; - return effect; - }); + this.firstEffect = this._effects[0]; + this.lastEffect = this._effects[this._effects.length - 1]; this._soundPlayers = new Set(); } clone () { const chain = new EffectChain(this.audioEngine, this.effects); - if (this.target === target) { - chain.connect(target); + if (this.target) { + chain.connect(this.target); } return chain; } @@ -46,17 +54,20 @@ class EffectChain { * @param {object} target - target whose node to should be connected */ connect (target) { - const {lastEffect} = this; + const {firstEffect, lastEffect} = this; + if (target === lastEffect) { this.inputNode.disconnect(); this.inputNode.connect(lastEffect.getInputNode()); + return; + } else if (target === firstEffect) { return; } this.target = target; - this._effects[0].connect(target); + firstEffect.connect(target); } From c12a1a4766b359ef91e0cb075e5890ca7055a534 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Mon, 18 Jun 2018 16:22:36 -0400 Subject: [PATCH 5/9] comment SoundBank and EffectChain --- src/SoundBank.js | 72 +++++++++++++++++++++++++++++++++- src/effects/EffectChain.js | 79 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 4 deletions(-) diff --git a/src/SoundBank.js b/src/SoundBank.js index ecbb1c4..044dd90 100644 --- a/src/SoundBank.js +++ b/src/SoundBank.js @@ -1,21 +1,64 @@ 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) { @@ -55,12 +112,22 @@ class SoundBank { }); } + /** + * 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) { @@ -69,6 +136,9 @@ class SoundBank { }); } + /** + * Dispose of all EffectChains and SoundPlayers. + */ dispose () { this.playerTargets.clear(); this.soundEffects.forEach(effects => effects.dispose()); diff --git a/src/effects/EffectChain.js b/src/effects/EffectChain.js index e0b175f..bef3775 100644 --- a/src/effects/EffectChain.js +++ b/src/effects/EffectChain.js @@ -1,15 +1,37 @@ 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 instantiate in reverse so that the first refers to the + // 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 .reverse() .map(Effect => { @@ -20,12 +42,28 @@ class EffectChain { }) .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) { @@ -34,6 +72,10 @@ class EffectChain { 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); @@ -41,16 +83,24 @@ class EffectChain { } } + /** + * 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 + * Connnect this player's output to another audio node. * @param {object} target - target whose node to should be connected */ connect (target) { @@ -70,11 +120,19 @@ class EffectChain { firstEffect.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) { @@ -85,20 +143,35 @@ class EffectChain { }); } + /** + * 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()); From 45a7d1abef0a0775cf13be991ea2033e68e43a55 Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Wed, 20 Jun 2018 15:31:28 -0400 Subject: [PATCH 6/9] rm AudioPlayer and SoundPlayer --- src/AudioEngine.js | 9 +-- src/AudioPlayer.js | 152 --------------------------------------------- src/SoundPlayer.js | 91 --------------------------- 3 files changed, 3 insertions(+), 249 deletions(-) delete mode 100644 src/AudioPlayer.js delete mode 100644 src/SoundPlayer.js diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 6781ef9..126b19e 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -5,7 +5,6 @@ const log = require('./log'); const uid = require('./uid'); const ADPCMSoundDecoder = require('./ADPCMSoundDecoder'); -const AudioPlayer = require('./AudioPlayer'); const Loudness = require('./Loudness'); const SoundPlayer = require('./GreenPlayer'); @@ -242,13 +241,11 @@ 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'); } 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/SoundPlayer.js b/src/SoundPlayer.js deleted file mode 100644 index 009b3d5..0000000 --- a/src/SoundPlayer.js +++ /dev/null @@ -1,91 +0,0 @@ -const log = require('./log'); - -/** - * A SoundPlayer stores an audio buffer, and plays it - */ -class SoundPlayer { - /** - * @param {AudioContext} audioContext - a webAudio context - * @constructor - */ - constructor (audioContext) { - this.audioContext = audioContext; - this.outputNode = null; - this.buffer = null; - this.bufferSource = null; - this.playbackRate = 1; - this.isPlaying = false; - } - - /** - * Connect the SoundPlayer to an output node - * @param {GainNode} node - an output node to connect to - */ - connect (node) { - this.outputNode = node; - } - - /** - * Set an audio buffer - * @param {AudioBuffer} buffer - Buffer to set - */ - setBuffer (buffer) { - this.buffer = buffer; - } - - /** - * 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. - */ - setPlaybackRate (playbackRate) { - this.playbackRate = playbackRate; - if (this.bufferSource && this.bufferSource.playbackRate) { - this.bufferSource.playbackRate.value = this.playbackRate; - } - } - - /** - * Stop the sound - */ - stop () { - if (this.bufferSource && this.isPlaying) { - this.bufferSource.stop(); - } - this.isPlaying = false; - } - - /** - * Start playing the sound - * The web audio framework requires a new audio buffer source node for each playback - */ - start () { - if (!this.buffer) { - log.warn('tried to play a sound that was not loaded yet'); - 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.isPlaying = true; - } - - /** - * 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 - */ - finished () { - return new Promise(resolve => { - this.bufferSource.onended = () => { - this.isPlaying = false; - resolve(); - }; - }); - } -} - -module.exports = SoundPlayer; From ea6093835c63c7c832a4e0fb1047d202df1d7901 Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Wed, 20 Jun 2018 15:32:20 -0400 Subject: [PATCH 7/9] mv GreenPlayer.js SoundPlayer.js --- src/AudioEngine.js | 2 +- src/{GreenPlayer.js => SoundPlayer.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{GreenPlayer.js => SoundPlayer.js} (100%) diff --git a/src/AudioEngine.js b/src/AudioEngine.js index 126b19e..f39746f 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -6,7 +6,7 @@ const uid = require('./uid'); const ADPCMSoundDecoder = require('./ADPCMSoundDecoder'); const Loudness = require('./Loudness'); -const SoundPlayer = require('./GreenPlayer'); +const SoundPlayer = require('./SoundPlayer'); const EffectChain = require('./effects/EffectChain'); const PanEffect = require('./effects/PanEffect'); diff --git a/src/GreenPlayer.js b/src/SoundPlayer.js similarity index 100% rename from src/GreenPlayer.js rename to src/SoundPlayer.js From c8f3510713f6bf96b5d97d0854a2f3ce2b32fcdd Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Thu, 21 Jun 2018 11:13:04 -0400 Subject: [PATCH 8/9] Better order of stop/connect/play --- src/SoundBank.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/SoundBank.js b/src/SoundBank.js index 044dd90..98ef0c4 100644 --- a/src/SoundBank.js +++ b/src/SoundBank.js @@ -90,11 +90,17 @@ class SoundBank { const effects = this.getSoundEffects(soundId); const player = this.getSoundPlayer(soundId); - this.playerTargets.set(soundId, target); - effects.setEffectsFromTarget(target); - effects.addSoundPlayer(player); + 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(); From 018d4fc01e231f07852720679a6c9330d8438d01 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 20 Jun 2018 15:27:57 -0400 Subject: [PATCH 9/9] add AudioEngine.createEffectChain --- src/AudioEngine.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/AudioEngine.js b/src/AudioEngine.js index f39746f..80122ab 100644 --- a/src/AudioEngine.js +++ b/src/AudioEngine.js @@ -248,11 +248,14 @@ class AudioEngine { log.warn('the createPlayer method is no longer available, please use createBank'); } - - createBank () { + createEffectChain () { const effects = new EffectChain(this, this.effects); effects.connect(this); - return new SoundBank(this, effects); + return effects; + } + + createBank () { + return new SoundBank(this, this.createEffectChain()); } }