mirror of
https://github.com/scratchfoundation/scratch-audio.git
synced 2024-12-22 22:12:48 -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 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
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;
|
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}
|
||||||
|
|
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;
|
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