Tone.js/Tone/instrument/MultiSampler.js
Yotam Mann 0a42b3ef54 adding triggerAttackRelease method
Instrument one does not work with MultiSampler since a note is required
as the first argument
2017-06-16 17:27:30 -04:00

220 lines
No EOL
6.3 KiB
JavaScript

define(["Tone/core/Tone", "Tone/instrument/Instrument", "Tone/core/Buffers", "Tone/source/BufferSource"],
function (Tone) {
/**
* @class Automatically interpolates between a set of pitched samples
* @param {Object} samples An object of samples mapping either Midi
* Note Numbers or Scientific Pitch Notation
* to the url of that sample.
* @example
* var sampler = new Tone.MultiSampler({
* "C3" : "path/to/C3.mp3",
* "D#3" : "path/to/Dsharp3.mp3",
* "F#3" : "path/to/Fsharp3.mp3",
* "A3" : "path/to/A3.mp3",
* }, function(){
* //sampler will repitch the closest sample
* sampler.triggerAttack("D3")
* })
*/
Tone.MultiSampler = function(urls){
// shift arguments over one. Those are the remainder of the options
var args = Array.prototype.slice.call(arguments);
args.shift();
var options = Tone.defaults(args, ["onload", "baseUrl"], Tone.MultiSampler);
Tone.Instrument.call(this, options);
var urlMap = {};
for (var note in urls){
if (Tone.isNote(note)){
//convert the note name to MIDI
var mid = Tone.Frequency(note).toMidi();
urlMap[mid] = urls[note];
} else if (!isNaN(parseFloat(note))){
//otherwise if it's numbers assume it's midi
urlMap[note] = urls[note];
} else {
throw new Error("Tone.MultiSampler: url keys must be the note's pitch");
}
}
/**
* The stored and loaded buffers
* @type {Tone.Buffers}
* @private
*/
this._buffers = new Tone.Buffers(urlMap, options.onload, options.baseUrl);
/**
* The object of all currently playing BufferSources
* @type {Object}
* @private
*/
this._activeSources = {};
/**
* The envelope applied to the beginning of the sample.
* @type {Time}
*/
this.attack = options.attack;
/**
* The envelope applied to the end of the envelope.
* @type {Time}
*/
this.release = options.release;
};
Tone.extend(Tone.MultiSampler, Tone.Instrument);
/**
* The defaults
* @const
* @type {Object}
*/
Tone.MultiSampler.defaults = {
attack : 0,
release : 0.1,
onload : Tone.noOp,
baseUrl : ""
};
/**
* Returns the difference in steps between the given midi note at the closets sample.
* @param {Midi} midi
* @return {Interval}
* @private
*/
Tone.MultiSampler.prototype._findClosest = function(midi){
var MAX_INTERVAL = 24;
var interval = 0;
while(interval < MAX_INTERVAL){
// check above and below
if (this._buffers.has(midi + interval)){
return -interval;
} else if (this._buffers.has(midi - interval)){
return interval;
}
interval++;
}
return null;
};
/**
* @param {Frequency} note The note to play
* @param {Time=} time When to play the note
* @param {NormalRange=} velocity The velocity to play the sample back.
* @return {Tone.MultiSampler} this
*/
Tone.MultiSampler.prototype.triggerAttack = function(note, time, velocity){
var midi = Tone.Frequency(note).toMidi();
// find the closest note pitch
var difference = this._findClosest(midi);
if (difference !== null){
var closestNote = midi - difference;
var buffer = this._buffers.get(closestNote);
// play that note
var source = new Tone.BufferSource({
"buffer" : buffer,
"playbackRate" : Tone.intervalToFrequencyRatio(difference),
"fadeIn" : this.attack,
"fadeOut" : this.release
}).connect(this.output);
source.start(time, 0, buffer.duration, velocity);
// add it to the active sources
if (!Tone.isArray(this._activeSources[midi])){
this._activeSources[midi] = [];
}
this._activeSources[midi].push({
note : midi,
source : source
});
}
return this;
};
/**
* @param {Frequency} note The note to release.
* @param {Time=} time When to release the note.
* @return {Tone.MultiSampler} this
*/
Tone.MultiSampler.prototype.triggerRelease = function(note, time){
var midi = Tone.Frequency(note).toMidi();
// find the note
if (this._activeSources[midi] && this._activeSources[midi].length){
var source = this._activeSources[midi].shift().source;
source.stop(time, this.release);
}
};
/**
* Invoke the attack phase, then after the duration, invoke the release.
* @param {Frequency} note The note to play
* @param {Time} duration The time the note should be held
* @param {Time=} time When to start the attack
* @param {NormalRange} [velocity=1] The velocity of the attack
* @return {Tone.MultiSampler} this
*/
Tone.MultiSampler.prototype.triggerAttackRelease = function(note, duration, time, velocity){
time = this.toSeconds(time);
duration = this.toSeconds(duration);
this.triggerAttack(note, time, velocity);
this.triggerRelease(note, time + duration);
return this;
};
/**
* Add a note to the sampler.
* @param {Note|Midi} note The buffer's pitch.
* @param {String|Tone.Buffer|Audiobuffer} url Either the url of the bufer,
* or a buffer which will be added
* with the given name.
* @param {Function=} callback The callback to invoke
* when the url is loaded.
*/
Tone.MultiSampler.prototype.add = function(note, url, callback){
if (Tone.isNote(note)){
//convert the note name to MIDI
var mid = Tone.Frequency(note).toMidi();
this._buffers.add(mid, url, callback);
} else if (!isNaN(parseFloat(note))){
//otherwise if it's numbers assume it's midi
this._buffers.add(note, url, callback);
} else {
throw new Error("Tone.MultiSampler: note must be the note's pitch. Instead got "+note);
}
};
/**
* If the buffers are loaded or not
* @memberOf Tone.MultiSampler#
* @type {Boolean}
* @name loaded
* @readOnly
*/
Object.defineProperty(Tone.MultiSampler.prototype, "loaded", {
get : function(){
return this._buffers.loaded;
}
});
/**
* Clean up
* @return {Tone.MultiSampler} this
*/
Tone.MultiSampler.prototype.dispose = function(){
Tone.Instrument.prototype.dispose.call(this);
this._buffers.dispose();
this._buffers = null;
for (var midi in this._activeSources){
this._activeSources[midi].forEach(function(event){
event.source.dispose();
});
}
this._activeSources = null;
return this;
};
return Tone.MultiSampler;
});