2016-10-13 14:54:07 -04:00
|
|
|
var Tone = require('tone');
|
|
|
|
var Soundfont = require('soundfont-player');
|
|
|
|
|
2016-10-14 10:26:08 -04:00
|
|
|
function AudioEngine (sounds) {
|
2016-10-13 14:54:07 -04:00
|
|
|
|
|
|
|
// tone setup
|
|
|
|
|
|
|
|
this.tone = new Tone();
|
|
|
|
|
|
|
|
// effects setup
|
2016-10-18 13:32:51 -04:00
|
|
|
// each effect has a single parameter controlled by the effects block
|
2016-10-13 14:54:07 -04:00
|
|
|
|
|
|
|
this.delay = new Tone.FeedbackDelay(0.25, 0.5);
|
|
|
|
this.panner = new Tone.Panner();
|
|
|
|
this.reverb = new Tone.Freeverb();
|
2016-10-18 13:32:51 -04:00
|
|
|
this.pitchShiftRatio;
|
2016-10-13 14:54:07 -04:00
|
|
|
|
2016-10-18 13:32:51 -04:00
|
|
|
// reset effects to their default parameters
|
2016-10-13 14:54:07 -04:00
|
|
|
this.clearEffects();
|
|
|
|
|
2016-10-18 13:32:51 -04:00
|
|
|
// the effects are chained to an effects node for this clone, then to the master output
|
|
|
|
// so audio is sent from each sampler or instrument, through the effects in order, then out
|
|
|
|
// note that the pitch effect works differently - it sets the playback rate for each sampler
|
2016-10-17 16:51:12 -04:00
|
|
|
this.effectsNode = new Tone.Gain();
|
|
|
|
this.effectsNode.chain(this.delay, this.panner, this.reverb, Tone.Master);
|
|
|
|
|
2016-10-13 14:54:07 -04:00
|
|
|
// drum sounds
|
|
|
|
|
|
|
|
// var drumFileNames = ['high_conga', 'small_cowbell',
|
|
|
|
// 'snare_drum', 'splash cymbal'];
|
|
|
|
// this.drumSamplers = this._loadSoundFiles(drumFileNames);
|
|
|
|
|
|
|
|
// sound urls - map each url to its tone.sampler
|
|
|
|
this.soundSamplers = [];
|
2016-10-14 10:26:08 -04:00
|
|
|
this.loadSounds(sounds);
|
2016-10-13 14:54:07 -04:00
|
|
|
|
2016-10-14 10:26:08 -04:00
|
|
|
// soundfont setup
|
2016-10-13 14:54:07 -04:00
|
|
|
|
|
|
|
// instrument names used by Musyng Kite soundfont, in order to
|
|
|
|
// match scratch instruments
|
|
|
|
this.instrumentNames = ['acoustic_grand_piano', 'electric_piano_1',
|
|
|
|
'drawbar_organ', 'acoustic_guitar_nylon', 'electric_guitar_clean',
|
|
|
|
'acoustic_bass', 'pizzicato_strings', 'cello', 'trombone', 'clarinet'];
|
|
|
|
|
2016-10-19 13:13:56 -04:00
|
|
|
this.setInstrument(0);
|
|
|
|
|
|
|
|
// theremin setup
|
|
|
|
|
|
|
|
this.theremin = new Tone.Synth();
|
|
|
|
this.portamentoTime = 0.25;
|
|
|
|
this.theremin.portamento = this.portamentoTime;
|
|
|
|
this.thereminVibrato = new Tone.Vibrato(4, 0.5);
|
|
|
|
this.theremin.chain(this.thereminVibrato, this.effectsNode);
|
|
|
|
this.thereminTimeout;
|
|
|
|
this.thereminIsPlaying = false;
|
2016-10-13 14:54:07 -04:00
|
|
|
}
|
|
|
|
|
2016-10-14 10:26:08 -04:00
|
|
|
AudioEngine.prototype.loadSounds = function (sounds) {
|
|
|
|
for (var i=0; i<sounds.length; i++) {
|
|
|
|
var url = sounds[i].fileUrl;
|
2016-10-17 16:51:12 -04:00
|
|
|
var sampler = new Tone.Sampler(url);
|
|
|
|
sampler.connect(this.effectsNode);
|
2016-10-17 15:41:56 -04:00
|
|
|
this.soundSamplers.push(sampler);
|
2016-10-14 10:26:08 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-10-17 15:41:56 -04:00
|
|
|
AudioEngine.prototype.playSound = function (index) {
|
|
|
|
this.soundSamplers[index].triggerAttack();
|
2016-10-19 13:13:56 -04:00
|
|
|
this.soundSamplers[index].player.playbackRate = 1 + this.pitchShiftRatio;
|
|
|
|
|
2016-10-13 14:54:07 -04:00
|
|
|
};
|
|
|
|
|
2016-10-17 15:41:56 -04:00
|
|
|
AudioEngine.prototype.getSoundDuration = function (index) {
|
|
|
|
return this.soundSamplers[index].player.buffer.duration;
|
2016-10-13 14:54:07 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
AudioEngine.prototype.playNoteForBeats = function (note, beats) {
|
|
|
|
this.instrument.play(
|
|
|
|
note, Tone.context.currentTime, {duration : Number(beats)}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2016-10-19 13:13:56 -04:00
|
|
|
AudioEngine.prototype.playThereminForBeats = function (note, beats) {
|
|
|
|
// if the theremin is playing
|
|
|
|
// set 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 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;
|
|
|
|
};
|
|
|
|
|
2016-10-13 14:54:07 -04:00
|
|
|
AudioEngine.prototype.playDrumForBeats = function (drumNum) {
|
|
|
|
this.drumSamplers[drumNum].triggerAttack();
|
|
|
|
};
|
|
|
|
|
|
|
|
AudioEngine.prototype.stopAllSounds = function () {
|
|
|
|
// stop drum notes
|
|
|
|
// for (var i = 0; i<this.drumSamplers.length; i++) {
|
|
|
|
// this.drumSamplers[i].triggerRelease();
|
|
|
|
// }
|
|
|
|
// stop sounds triggered with playSound (indexed by their urls)
|
|
|
|
for (var key in this.soundSamplers) {
|
|
|
|
this.soundSamplers[key].triggerRelease();
|
|
|
|
}
|
|
|
|
// stop soundfont notes
|
|
|
|
this.instrument.stop();
|
|
|
|
};
|
|
|
|
|
|
|
|
AudioEngine.prototype.setEffect = function (effect, value) {
|
|
|
|
switch (effect) {
|
|
|
|
case 'ECHO':
|
|
|
|
this.delay.wet.value = (value / 100) / 2; // max 50% wet
|
|
|
|
break;
|
|
|
|
case 'PAN':
|
|
|
|
this.panner.pan.value = value / 100;
|
|
|
|
break;
|
|
|
|
case 'REVERB':
|
|
|
|
this.reverb.wet.value = value / 100;
|
|
|
|
break;
|
|
|
|
case 'PITCH':
|
|
|
|
this._setPitchShift(value / 20);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
AudioEngine.prototype.changeEffect = function (effect, value) {
|
|
|
|
switch (effect) {
|
|
|
|
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);
|
|
|
|
break;
|
|
|
|
case 'PAN':
|
|
|
|
this.panner.pan.value += value / 100;
|
|
|
|
this.panner.pan.value = this._clamp(this.panner.pan.value, -1, 1);
|
|
|
|
break;
|
|
|
|
case 'REVERB':
|
|
|
|
this.reverb.wet.value += value / 100;
|
|
|
|
this.reverb.wet.value = this._clamp(this.reverb.wet.value, 0, 1);
|
|
|
|
break;
|
|
|
|
case 'PITCH':
|
2016-10-17 16:51:12 -04:00
|
|
|
this._setPitchShift(this.pitchShiftRatio + (value / 20));
|
2016-10-13 14:54:07 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
AudioEngine.prototype._setPitchShift = function (value) {
|
2016-10-17 16:51:12 -04:00
|
|
|
this.pitchShiftRatio = value;
|
2016-10-13 14:54:07 -04:00
|
|
|
for (var i in this.soundSamplers) {
|
2016-10-17 16:51:12 -04:00
|
|
|
this.soundSamplers[i].player.playbackRate = 1 + this.pitchShiftRatio;
|
2016-10-13 14:54:07 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-10-19 13:13:56 -04:00
|
|
|
AudioEngine.prototype.setInstrument = function (instrumentNum) {
|
|
|
|
return Soundfont.instrument(Tone.context, this.instrumentNames[instrumentNum]).then(
|
|
|
|
function (inst) {
|
|
|
|
this.instrument = inst;
|
|
|
|
this.instrument.connect(this.effectsNode);
|
|
|
|
}.bind(this)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2016-10-13 14:54:07 -04:00
|
|
|
AudioEngine.prototype.clearEffects = function () {
|
|
|
|
this.delay.wet.value = 0;
|
|
|
|
this._setPitchShift(0);
|
|
|
|
this.panner.pan.value = 0;
|
|
|
|
this.reverb.wet.value = 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
AudioEngine.prototype._clamp = function (input, min, max) {
|
|
|
|
return Math.min(Math.max(input, min), max);
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = AudioEngine;
|
|
|
|
|