make objects for each effect

This commit is contained in:
Eric Rosenbaum 2016-11-29 18:33:09 -05:00
parent f797ec80f3
commit 9120c3d430
9 changed files with 369 additions and 264 deletions

52
src/effects/EchoEffect.js Normal file
View file

@ -0,0 +1,52 @@
/*
An echo effect
0 mutes the effect
Values up to 100 set the echo feedback amount,
increasing the time it takes the echo to fade away
Clamped 0-100
*/
var Tone = require('tone');
function EchoEffect () {
Tone.Effect.call(this);
this.value = 0;
this.delay = new Tone.FeedbackDelay(0.25, 0.5);
this.effectSend.chain(this.delay, this.effectReturn);
}
Tone.extend(EchoEffect, Tone.Effect);
EchoEffect.prototype.set = function (val) {
this.value = val;
this.value = this.clamp(this.value, 0, 100);
// mute the effect if value is 0
if (this.value == 0) {
this.wet.value = 0;
} else {
this.wet.value = 1;
}
var feedback = (this.value / 100) * 0.75;
this.delay.feedback.rampTo(feedback, 1/60);
};
EchoEffect.prototype.changeBy = function (val) {
this.set(this.value + val);
};
EchoEffect.prototype.clamp = function (input, min, max) {
return Math.min(Math.max(input, min), max);
};
module.exports = EchoEffect;

44
src/effects/FuzzEffect.js Normal file
View file

@ -0,0 +1,44 @@
/*
A fuzz effect
Distortion
the value controls the wet/dry amount
Clamped 0-100
*/
var Tone = require('tone');
function FuzzEffect () {
Tone.Effect.call(this);
this.value = 0;
this.distortion = new Tone.Distortion(1);
this.effectSend.chain(this.distortion, this.effectReturn);
}
Tone.extend(FuzzEffect, Tone.Effect);
FuzzEffect.prototype.set = function (val) {
this.value = val;
this.value = this.clamp(this.value, 0, 100);
this.distortion.wet.value = this.value / 100;
};
FuzzEffect.prototype.changeBy = function (val) {
this.set(this.value + val);
};
FuzzEffect.prototype.clamp = function (input, min, max) {
return Math.min(Math.max(input, min), max);
};
module.exports = FuzzEffect;

42
src/effects/PanEffect.js Normal file
View file

@ -0,0 +1,42 @@
/*
A Pan effect
-100 puts the audio on the left channel, 0 centers it, 100 makes puts it on the right.
Clamped -100 to 100
*/
var Tone = require('tone');
function PanEffect () {
Tone.Effect.call(this);
this.value = 0;
this.panner = new Tone.Panner();
this.effectSend.chain(this.panner, this.effectReturn);
}
Tone.extend(PanEffect, Tone.Effect);
PanEffect.prototype.set = function (val) {
this.value = val;
this.value = this.clamp(this.value, -100, 100);
this.panner.pan.value = this.value / 100;
};
PanEffect.prototype.changeBy = function (val) {
this.set(this.value + val);
};
PanEffect.prototype.clamp = function (input, min, max) {
return Math.min(Math.max(input, min), max);
};
module.exports = PanEffect;

View file

@ -0,0 +1,39 @@
/*
A Pitch effect
*/
var Tone = require('tone');
function PitchEffect () {
this.value = 0;
this.tone = new Tone();
}
PitchEffect.prototype.set = function (val, players) {
this.value = val;
this.updatePlayers(players);
};
PitchEffect.prototype.changeBy = function (val, players) {
this.set(this.value + val, players);
};
PitchEffect.prototype.getRatio = function () {
return this.tone.intervalToFrequencyRatio(this.value / 10);
};
PitchEffect.prototype.updatePlayers = function (players) {
if (!players) return;
var ratio = this.getRatio();
for (var i=0; i<players.length; i++) {
players[i].setPlaybackRate(ratio);
}
};
module.exports = PitchEffect;

View file

@ -0,0 +1,42 @@
/*
A Reverb effect
The value controls the wet/dry amount of the effect
Clamped 0 to 100
*/
var Tone = require('tone');
function ReverbEffect () {
Tone.Effect.call(this);
this.value = 0;
this.reverb = new Tone.Freeverb();
this.effectSend.chain(this.reverb, this.effectReturn);
}
Tone.extend(ReverbEffect, Tone.Effect);
ReverbEffect.prototype.set = function (val) {
this.value = val;
this.value = this.clamp(this.value, 0, 100);
this.reverb.wet.value = this.value / 100;
};
ReverbEffect.prototype.changeBy = function (val) {
this.set(this.value + val);
};
ReverbEffect.prototype.clamp = function (input, min, max) {
return Math.min(Math.max(input, min), max);
};
module.exports = ReverbEffect;

View file

@ -0,0 +1,59 @@
/*
A robot-voice effect
A feedback comb filter with a short delay time creates a low-pitched buzzing
The effect value controls the length of this delay time, changing the pitch
0 mutes the effect
Other values changes the pitch of the effect, in units of 10 steps per semitone
Not clamped
*/
var Tone = require('tone');
function RoboticEffect () {
Tone.Effect.call(this);
this.value = 0;
var time = this._delayTimeForValue(100);
this.feedbackCombFilter = new Tone.FeedbackCombFilter(time, 0.9);
this.effectSend.chain(this.feedbackCombFilter, this.effectReturn);
}
Tone.extend(RoboticEffect, Tone.Effect);
RoboticEffect.prototype.set = function (val) {
this.value = val;
// mute the effect if value is 0
if (this.value == 0) {
this.wet.value = 0;
} else {
this.wet.value = 1;
}
// set delay time using the value
var time = this._delayTimeForValue(this.value);
this.feedbackCombFilter.delayTime.rampTo(time, 1/60);
};
RoboticEffect.prototype.changeBy = function (val) {
this.set(this.value + val);
};
RoboticEffect.prototype._delayTimeForValue = function (val) {
// convert effect setting range, typically 0-100 but can be outside that,
// to a musical note, and return the period of the frequency of that note
var midiNote = ((val - 100) / 10) + 36;
var freq = Tone.Frequency(midiNote, 'midi').eval();
return 1 / freq;
};
module.exports = RoboticEffect;

View file

@ -0,0 +1,47 @@
/*
A wobble effect
A low frequency oscillator (LFO) controls a gain node
This creates an effect like tremolo
Clamped 0 to 100
*/
var Tone = require('tone');
function WobbleEffect () {
Tone.Effect.call(this);
this.value = 0;
this.wobbleLFO = new Tone.LFO(10, 0, 1).start();
this.wobbleGain = new Tone.Gain();
this.wobbleLFO.connect(this.wobbleGain.gain);
this.effectSend.chain(this.wobbleGain, this.effectReturn);
}
Tone.extend(WobbleEffect, Tone.Effect);
WobbleEffect.prototype.set = function (val) {
this.value = val;
this.value = this.clamp(this.value, 0, 100);
this.wet.value = this.value / 100;
this.wobbleLFO.frequency.rampTo(this.value / 10, 1/60);
};
WobbleEffect.prototype.changeBy = function (val) {
this.set(this.value + val);
};
WobbleEffect.prototype.clamp = function (input, min, max) {
return Math.min(Math.max(input, min), max);
};
module.exports = WobbleEffect;

View file

@ -1,5 +1,14 @@
var log = require('./log');
var Tone = require('tone');
var PitchEffect = require('./effects/PitchEffect');
var EchoEffect = require('./effects/EchoEffect');
var PanEffect = require('./effects/PanEffect');
var RoboticEffect = require('./effects/RoboticEffect');
var ReverbEffect = require('./effects/ReverbEffect');
var FuzzEffect = require('./effects/FuzzEffect');
var WobbleEffect = require('./effects/WobbleEffect');
var SoundPlayer = require('./SoundPlayer');
var Soundfont = require('soundfont-player');
var ADPCMSoundLoader = require('./ADPCMSoundLoader');
@ -11,52 +20,31 @@ function AudioEngine (sounds) {
this.tone = new Tone();
// effects setup
// each effect has a single parameter controlled by the effects block
this.delay = new Tone.FeedbackDelay(0.25, 0.5);
this.panner = new Tone.Panner();
this.reverb = new Tone.Freeverb();
this.distortion = new Tone.Distortion(1);
this.pitchEffectValue;
this.robotic = new Tone.Effect();
// use the period of a musical note to set the feedback filter delay time
var delayTime = 1 / Tone.Frequency('Gb3').eval();
var robotFilter = new Tone.FeedbackCombFilter(delayTime, 0.9);
this.robotic.effectSend.chain(robotFilter, this.robotic.effectReturn);
this.wobble = new Tone.Effect();
var wobbleLFO = new Tone.LFO(10, 0, 1).start();
var wobbleGain = new Tone.Gain();
wobbleLFO.connect(wobbleGain.gain);
this.wobble.effectSend.chain(wobbleGain, this.wobble.effectReturn);
// telephone effect - simulating the 'tinny' sound coming over a phone line
// using a lowpass filter and a highpass filter
this.telephone = new Tone.Effect();
var telephoneLP = new Tone.Filter(1200, 'lowpass', -24);
var telephoneHP = new Tone.Filter(800, 'highpass', -24);
this.telephone.effectSend.chain(telephoneLP, telephoneHP, this.telephone.effectReturn);
this.pitchEffect = new PitchEffect();
this.echoEffect = new EchoEffect();
this.panEffect = new PanEffect();
this.reverbEffect = new ReverbEffect();
this.fuzzEffect = new FuzzEffect();
this.wobbleEffect = new WobbleEffect();
this.roboticEffect = new RoboticEffect();
// the effects are chained to an effects node for this clone, then to the master output
// so audio is sent from each player or instrument, through the effects in order, then out
// note that the pitch effect works differently - it sets the playback rate for each player
this.effectsNode = new Tone.Gain();
this.effectsNode.chain(
this.robotic, this.distortion, this.delay, this.telephone,
this.wobble, this.panner, this.reverb, Tone.Master);
this.roboticEffect, this.fuzzEffect, this.echoEffect,
this.wobbleEffect, this.panEffect, this.reverbEffect, Tone.Master);
// reset effects to their default parameters
this.clearEffects();
this.effectNames = ['PITCH', 'PAN', 'ECHO', 'REVERB', 'FUZZ', 'TELEPHONE', 'WOBBLE', 'ROBOTIC'];
// load sounds
this.soundPlayers = [];
this.loadSounds(sounds);
// Tone.Buffer.on('load', this._soundsLoaded.bind(this));
// soundfont setup
@ -69,21 +57,12 @@ function AudioEngine (sounds) {
'music_box', 'steel_drums', 'marimba', 'lead_1_square', 'fx_4_atmosphere'];
this.instrumentNum;
this.setInstrument(0);
this.setInstrument(1);
// tempo in bpm (beats per minute)
// default is 60bpm
this.currentTempo = 60;
// theremin setup
this.theremin = new Tone.Synth();
this.portamentoTime = 0.25;
this.thereminVibrato = new Tone.Vibrato(4, 0.5);
this.theremin.chain(this.thereminVibrato, this.effectsNode);
this.thereminTimeout;
this.thereminIsPlaying = false;
}
AudioEngine.prototype.loadSounds = function (sounds) {
@ -151,30 +130,6 @@ AudioEngine.prototype.playNoteForBeats = function (note, beats) {
*/
};
AudioEngine.prototype.playThereminForBeats = function (note, beats) {
// if the theremin is playing
// ramp to new frequency
// else
// trigger attack
// create a timeout for slightly longer than the duration of the block
// that releases the theremin - so we can slide continuously between
// successive notes without releasing and re-attacking
var freq = this._midiToFreq(note);
if (this.thereminIsPlaying) {
this.theremin.frequency.rampTo(freq, this.portamentoTime);
} else {
this.theremin.triggerAttack(freq);
this.thereminIsPlaying = true;
}
clearTimeout(this.thereminTimeout);
this.thereminTimeout = setTimeout(function () {
this.theremin.triggerRelease();
this.thereminIsPlaying = false;
}.bind(this), (1000 * beats) + 100);
};
AudioEngine.prototype._midiToFreq = function (midiNote) {
var freq = this.tone.intervalToFrequencyRatio(midiNote - 60) * 261.63; // 60 is C4
return freq;
@ -204,28 +159,25 @@ AudioEngine.prototype.stopAllSounds = function () {
AudioEngine.prototype.setEffect = function (effect, value) {
switch (effect) {
case 'PITCH':
this._setPitchShift(value);
this.pitchEffect.set(value, this.soundPlayers);
break;
case 'PAN':
this.panner.pan.value = value / 100;
this.panEffect.set(value);
break;
case 'ECHO':
this.delay.wet.value = (value / 100) / 2; // max 50% wet
this.echoEffect.set(value);
break;
case 'REVERB':
this.reverb.wet.value = value / 100;
this.reverbEffect.set(value);
break;
case 'FUZZ' :
this.distortion.wet.value = value / 100;
break;
case 'TELEPHONE' :
this.telephone.wet.value = value / 100;
this.fuzzEffect.set(value);
break;
case 'WOBBLE' :
this.wobble.wet.value = value / 100;
this.wobbleEffect.set(value);
break;
case 'ROBOTIC' :
this.robotic.wet.value = value / 100;
this.roboticEffect.set(value);
break;
}
};
@ -233,66 +185,46 @@ AudioEngine.prototype.setEffect = function (effect, value) {
AudioEngine.prototype.changeEffect = function (effect, value) {
switch (effect) {
case 'PITCH':
this._setPitchShift(this.pitchEffectValue + Number(value));
this.pitchEffect.changeBy(value, this.soundPlayers);
break;
case 'PAN':
this.panner.pan.value += value / 100;
this.panner.pan.value = this._clamp(this.panner.pan.value, -1, 1);
this.panEffect.changeBy(value);
break;
case 'ECHO':
this.delay.wet.value += (value / 100) / 2; // max 50% wet
this.delay.wet.value = this._clamp(this.delay.wet.value, 0, 0.5);
this.echoEffect.changeBy(value);
break;
case 'REVERB':
this.reverb.wet.value += value / 100;
this.reverb.wet.value = this._clamp(this.reverb.wet.value, 0, 1);
this.reverbEffect.changeBy(value);
break;
case 'FUZZ' :
this.distortion.wet.value += value / 100;
this.distortion.wet.value = this._clamp(this.distortion.wet.value, 0, 1);
break;
case 'TELEPHONE' :
this.telephone.wet.value += value / 100;
this.telephone.wet.value = this._clamp(this.telephone.wet.value, 0, 1);
this.fuzzEffect.changeBy(value);
break;
case 'WOBBLE' :
this.wobble.wet.value += value / 100;
this.wobble.wet.value = this._clamp(this.wobble.wet.value, 0, 1);
this.wobbleEffect.changeBy(value);
break;
case 'ROBOTIC' :
this.robotic.wet.value += value / 100;
this.robotic.wet.value = this._clamp(this.robotic.wet.value, 0, 1);
this.roboticEffect.changeBy(value);
break;
}
};
AudioEngine.prototype._setPitchShift = function (value) {
this.pitchEffectValue = value;
AudioEngine.prototype.clearEffects = function () {
this.echoEffect.set(0);
this.panEffect.set(0);
this.reverbEffect.set(0);
this.fuzzEffect.set(0);
this.roboticEffect.set(0);
this.wobbleEffect.set(0);
this.pitchEffect.set(0, this.soundPlayers);
if (!this.soundPlayers) {
return;
}
var ratio = this._getPitchRatio();
this._setPlaybackRateForAllSoundPlayers(ratio);
};
AudioEngine.prototype._setPlaybackRateForAllSoundPlayers = function (rate) {
for (var i=0; i<this.soundPlayers.length; i++) {
this.soundPlayers[i].setPlaybackRate(rate);
}
};
AudioEngine.prototype._getPitchRatio = function () {
return this.tone.intervalToFrequencyRatio(this.pitchEffectValue / 10);
this.effectsNode.gain.value = 1;
};
AudioEngine.prototype.setInstrument = function (instrumentNum) {
this.instrumentNum = instrumentNum;
this.instrumentNum = instrumentNum - 1;
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]).then(
return Soundfont.instrument(Tone.context, this.instrumentNames[this.instrumentNum]).then(
function (inst) {
this.instrument = inst;
this.instrument.connect(this.effectsNode);
@ -300,19 +232,6 @@ AudioEngine.prototype.setInstrument = function (instrumentNum) {
);
};
AudioEngine.prototype.clearEffects = function () {
this.delay.wet.value = 0;
this.panner.pan.value = 0;
this.reverb.wet.value = 0;
this.distortion.wet.value = 0;
this.robotic.wet.value = 0;
this.wobble.wet.value = 0;
this.telephone.wet.value = 0;
this._setPitchShift(0);
this.effectsNode.gain.value = 1;
};
AudioEngine.prototype.setVolume = function (value) {
var vol = this._clamp(value, 0, 100);
vol /= 100;

View file

@ -1,139 +0,0 @@
/*
This is a port of Jamison Dance's port of Chris Wilson's WebAudio vocoder:
https://github.com/jergason/Vocoder
https://github.com/cwilso/Vocoder
I have adapted it to use the ToneJs library, making it a Tone Effect that
can be added to an audio effects chain.
*/
/*
The MIT License (MIT)
Copyright (c) 2014 Chris Wilson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var Tone = require('tone');
function Vocoder () {
Tone.Effect.call(this);
this.modulatorInput = new Tone.Gain();
this.carrierInput = new Tone.Gain();
this.outputGain = new Tone.Gain();
this.oscillatorNode;
this.vocoderBands = this.generateVocoderBands(55, 7040, 8);
this.initBandpassFilters();
this.createCarrier();
this.effectSend.connect(this.modulatorInput);
this.outputGain.connect(this.effectReturn);
}
Tone.extend(Vocoder, Tone.Effect);
Vocoder.prototype.setCarrierOscFrequency = function (freq) {
this.oscillatorNode.frequency.rampTo(freq, 0.05);
};
// this function algorithmically calculates vocoder bands, distributing evenly
// from startFreq to endFreq, splitting evenly (logarhythmically) into a given numBands.
Vocoder.prototype.generateVocoderBands = function (startFreq, endFreq, numBands) {
// Remember: 1200 cents in octave, 100 cents per semitone
var totalRangeInCents = 1200 * Math.log( endFreq / startFreq ) / Math.LN2;
var centsPerBand = totalRangeInCents / numBands;
var scale = Math.pow( 2, centsPerBand / 1200 ); // This is the scaling for successive bands
var vocoderBands = [];
var currentFreq = startFreq;
for (var i=0; i<numBands; i++) {
vocoderBands[i] = new Object();
vocoderBands[i].frequency = currentFreq;
currentFreq = currentFreq * scale;
}
return vocoderBands;
};
Vocoder.prototype.initBandpassFilters = function () {
var FILTER_QUALITY = 6; // The Q value for the carrier and modulator filters
// Set up a high-pass filter to add back in the fricatives, etc.
var hpFilter = new Tone.Filter(8000, 'highpass');
this.modulatorInput.connect(hpFilter);
hpFilter.connect(this.outputGain);
for (var i=0; i<this.vocoderBands.length; i++) {
// CREATE THE MODULATOR CHAIN
// create the bandpass filter in the modulator chain
var modulatorFilter = new Tone.Filter(this.vocoderBands[i].frequency, 'bandpass', -24);
modulatorFilter.Q.value = FILTER_QUALITY;
this.modulatorInput.connect(modulatorFilter);
// create a post-filtering gain to bump the levels up.
var modulatorFilterPostGain = new Tone.Gain(6);
modulatorFilter.connect(modulatorFilterPostGain);
// add a rectifier with a lowpass filter to turn the bandpass filtered signal
// into a smoothed control signal for the carrier filter
var rectifier = new Tone.WaveShaper([1,0,1]);
modulatorFilterPostGain.connect(rectifier);
var rectifierLowPass = new Tone.Filter(50, 'lowpass');
rectifier.connect(rectifierLowPass);
// Create the bandpass filter in the carrier chain
var carrierFilter = new Tone.Filter(this.vocoderBands[i].frequency, 'bandpass', -24);
carrierFilter.Q.value = FILTER_QUALITY;
this.carrierInput.connect(carrierFilter);
var carrierFilterPostGain = new Tone.Gain(10);
carrierFilter.connect(carrierFilterPostGain);
// Create the carrier band gain node
var bandGain = new Tone.Gain(0);
carrierFilterPostGain.connect(bandGain);
// the modulator band filter output controls the gain of the carrier band
rectifierLowPass.connect(bandGain.gain);
bandGain.connect(this.outputGain);
}
};
Vocoder.prototype.createCarrier = function () {
this.oscillatorNode = new Tone.Oscillator('C3', 'sawtooth').start();
this.oscillatorNode.connect(this.carrierInput);
var noiseNode = new Tone.Noise('white').start();
noiseNode.volume.value = -12;
noiseNode.connect(this.carrierInput);
};
module.exports = Vocoder;