Tone.js/Tone/instrument/Sampler.js

244 lines
7.3 KiB
JavaScript
Raw Normal View History

2018-01-02 15:37:27 +00:00
define(["Tone/core/Tone", "Tone/instrument/Instrument", "Tone/core/Buffers", "Tone/source/BufferSource"], function(Tone) {
2014-08-20 20:52:14 +00:00
/**
* @class Automatically interpolates between a set of pitched samples. Pass in an object which maps the note's pitch or midi value to the url, then you can trigger the attack and release of that note like other instruments. By automatically repitching the samples, it is possible to play pitches which were not explicitly included which can save loading time.
* For sample or buffer playback where repitching is not necessary, use [Tone.Player](https://tonejs.github.io/docs/Player).
* @param {Object} samples An object of samples mapping either Midi
* Note Numbers or Scientific Pitch Notation
* to the url of that sample.
* @param {Function=} onload The callback to invoke when all of the samples are loaded.
* @param {String=} baseUrl The root URL of all of the samples, which is prepended to all the URLs.
* @example
* var sampler = new Tone.Sampler({
* "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")
* })
* @extends {Tone.Instrument}
2014-08-20 20:52:14 +00:00
*/
Tone.Sampler = 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.Sampler);
Tone.Instrument.call(this, options);
2014-08-20 20:52:14 +00:00
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.Sampler: url keys must be the note's pitch");
}
}
2014-08-20 20:52:14 +00:00
/**
* The stored and loaded buffers
* @type {Tone.Buffers}
* @private
2014-08-20 20:52:14 +00:00
*/
this._buffers = new Tone.Buffers(urlMap, options.onload, options.baseUrl);
2014-08-20 20:52:14 +00:00
/**
* The object of all currently playing BufferSources
* @type {Object}
* @private
2014-08-20 20:52:14 +00:00
*/
this._activeSources = {};
2014-09-12 00:36:07 +00:00
/**
* 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;
2014-08-20 20:52:14 +00:00
};
2014-09-20 22:57:44 +00:00
Tone.extend(Tone.Sampler, Tone.Instrument);
2014-08-20 20:52:14 +00:00
/**
* The defaults
* @const
* @type {Object}
*/
Tone.Sampler.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
2014-08-20 20:52:14 +00:00
*/
Tone.Sampler.prototype._findClosest = function(midi){
var MAX_INTERVAL = 24;
var interval = 0;
2018-01-02 15:37:27 +00:00
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;
2014-08-20 20:52:14 +00:00
};
/**
* @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.Sampler} this
2014-08-20 20:52:14 +00:00
*/
Tone.Sampler.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,
2017-09-15 13:07:09 +00:00
"fadeOut" : this.release,
"curve" : "exponential",
}).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;
2014-08-20 20:52:14 +00:00
};
/**
* @param {Frequency} note The note to release.
* @param {Time=} time When to release the note.
* @return {Tone.Sampler} this
*/
Tone.Sampler.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;
time = this.toSeconds(time);
source.stop(time + this.release, this.release);
}
return this;
};
/**
* Release all currently active notes.
* @param {Time=} time When to release the notes.
* @return {Tone.Sampler} this
*/
Tone.Sampler.prototype.releaseAll = function(time){
time = this.toSeconds(time);
for (var note in this._activeSources){
var sources = this._activeSources[note];
2018-01-02 15:37:27 +00:00
while (sources.length){
var source = sources.shift().source;
source.stop(time + this.release, this.release);
}
}
return this;
};
2015-02-10 16:40:04 +00:00
/**
* 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.Sampler} this
2015-02-10 16:40:04 +00:00
*/
Tone.Sampler.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;
};
2015-02-10 16:40:04 +00:00
2015-05-13 03:48:13 +00:00
/**
* 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.
2015-05-13 03:48:13 +00:00
*/
Tone.Sampler.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.Sampler: note must be the note's pitch. Instead got "+note);
2015-02-10 21:33:37 +00:00
}
};
2015-02-10 21:33:37 +00:00
2016-07-20 19:42:55 +00:00
/**
* If the buffers are loaded or not
2016-07-20 19:42:55 +00:00
* @memberOf Tone.Sampler#
* @type {Boolean}
* @name loaded
* @readOnly
2016-07-20 19:42:55 +00:00
*/
Object.defineProperty(Tone.Sampler.prototype, "loaded", {
2016-07-20 19:42:55 +00:00
get : function(){
return this._buffers.loaded;
2016-07-20 19:42:55 +00:00
}
});
2014-08-20 20:52:14 +00:00
/**
* Clean up
* @return {Tone.Sampler} this
2014-08-20 20:52:14 +00:00
*/
Tone.Sampler.prototype.dispose = function(){
2014-09-20 22:57:44 +00:00
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;
2014-08-20 20:52:14 +00:00
};
return Tone.Sampler;
});