mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2024-12-22 14:02:29 -05:00
First Draft SoundBank + EffectChain
This commit is contained in:
parent
444aba7f76
commit
f5c219ceb3
7 changed files with 205 additions and 0 deletions
|
@ -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;
|
||||
|
|
84
src/SoundBank.js
Normal file
84
src/SoundBank.js
Normal 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;
|
|
@ -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}
|
||||
|
|
84
src/effects/EffectChain.js
Normal file
84
src/effects/EffectChain.js
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue