UNPKG

tone

Version:

A Web Audio framework for making interactive music in the browser.

269 lines 10.4 kB
import * as tslib_1 from "tslib"; import { ToneAudioBuffers } from "../core/context/ToneAudioBuffers"; import { intervalToFrequencyRatio } from "../core/type/Conversions"; import { FrequencyClass } from "../core/type/Frequency"; import { optionsFromArguments } from "../core/util/Defaults"; import { noOp } from "../core/util/Interface"; import { isArray, isNote, isNumber } from "../core/util/TypeCheck"; import { Instrument } from "../instrument/Instrument"; import { ToneBufferSource } from "../source/buffer/ToneBufferSource"; import { timeRange } from "../core/util/Decorator"; import { assert } from "../core/util/Debug"; /** * 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 [[Player]]. * @example * import { Sampler } from "tone"; * const sampler = new Sampler({ * urls: { * C1: "C1.mp3", * C2: "C2.mp3", * }, * baseUrl: "https://tonejs.github.io/examples/audio/casio/", * onload: () => { * sampler.triggerAttackRelease(["C1", "E1", "G1", "B1"], 0.5); * }, * }); * @category Instrument */ var Sampler = /** @class */ (function (_super) { tslib_1.__extends(Sampler, _super); function Sampler() { var _this = _super.call(this, optionsFromArguments(Sampler.getDefaults(), arguments, ["urls", "onload", "baseUrl"], "urls")) || this; _this.name = "Sampler"; /** * The object of all currently playing BufferSources */ _this._activeSources = new Map(); var options = optionsFromArguments(Sampler.getDefaults(), arguments, ["urls", "onload", "baseUrl"], "urls"); var urlMap = {}; Object.keys(options.urls).forEach(function (note) { var noteNumber = parseInt(note, 10); assert(isNote(note) || (isNumber(noteNumber) && isFinite(noteNumber)), "url key is neither a note or midi pitch: " + note); if (isNote(note)) { // convert the note name to MIDI var mid = new FrequencyClass(_this.context, note).toMidi(); urlMap[mid] = options.urls[note]; } else if (isNumber(noteNumber) && isFinite(noteNumber)) { // otherwise if it's numbers assume it's midi urlMap[noteNumber] = options.urls[noteNumber]; } }); _this._buffers = new ToneAudioBuffers({ urls: urlMap, onload: options.onload, baseUrl: options.baseUrl, onerror: options.onerror, }); _this.attack = options.attack; _this.release = options.release; _this.curve = options.curve; // invoke the callback if it's already loaded if (_this._buffers.loaded) { // invoke onload deferred Promise.resolve().then(options.onload); } return _this; } Sampler.getDefaults = function () { return Object.assign(Instrument.getDefaults(), { attack: 0, baseUrl: "", curve: "exponential", onload: noOp, onerror: noOp, release: 0.1, urls: {}, }); }; /** * Returns the difference in steps between the given midi note at the closets sample. */ Sampler.prototype._findClosest = function (midi) { // searches within 8 octaves of the given midi note var MAX_INTERVAL = 96; 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++; } throw new Error("No available buffers for note: " + midi); }; /** * @param notes The note to play, or an array of notes. * @param time When to play the note * @param velocity The velocity to play the sample back. */ Sampler.prototype.triggerAttack = function (notes, time, velocity) { var _this = this; if (velocity === void 0) { velocity = 1; } this.log("triggerAttack", notes, time, velocity); if (!Array.isArray(notes)) { notes = [notes]; } notes.forEach(function (note) { var midi = new FrequencyClass(_this.context, note).toMidi(); // find the closest note pitch var difference = _this._findClosest(midi); var closestNote = midi - difference; var buffer = _this._buffers.get(closestNote); var playbackRate = intervalToFrequencyRatio(difference); // play that note var source = new ToneBufferSource({ url: buffer, context: _this.context, curve: _this.curve, fadeIn: _this.attack, fadeOut: _this.release, playbackRate: playbackRate, }).connect(_this.output); source.start(time, 0, buffer.duration / playbackRate, velocity); // add it to the active sources if (!isArray(_this._activeSources.get(midi))) { _this._activeSources.set(midi, []); } _this._activeSources.get(midi).push(source); // remove it when it's done source.onended = function () { if (_this._activeSources && _this._activeSources.has(midi)) { var sources = _this._activeSources.get(midi); var index = sources.indexOf(source); if (index !== -1) { sources.splice(index, 1); } } }; }); return this; }; /** * @param notes The note to release, or an array of notes. * @param time When to release the note. */ Sampler.prototype.triggerRelease = function (notes, time) { var _this = this; this.log("triggerRelease", notes, time); if (!Array.isArray(notes)) { notes = [notes]; } notes.forEach(function (note) { var midi = new FrequencyClass(_this.context, note).toMidi(); // find the note if (_this._activeSources.has(midi) && _this._activeSources.get(midi).length) { var sources = _this._activeSources.get(midi); time = _this.toSeconds(time); sources.forEach(function (source) { source.stop(time); }); _this._activeSources.set(midi, []); } }); return this; }; /** * Release all currently active notes. * @param time When to release the notes. */ Sampler.prototype.releaseAll = function (time) { var computedTime = this.toSeconds(time); this._activeSources.forEach(function (sources) { while (sources.length) { var source = sources.shift(); source.stop(computedTime); } }); return this; }; Sampler.prototype.sync = function () { this._syncMethod("triggerAttack", 1); this._syncMethod("triggerRelease", 1); return this; }; /** * Invoke the attack phase, then after the duration, invoke the release. * @param notes The note to play and release, or an array of notes. * @param duration The time the note should be held * @param time When to start the attack * @param velocity The velocity of the attack */ Sampler.prototype.triggerAttackRelease = function (notes, duration, time, velocity) { var _this = this; if (velocity === void 0) { velocity = 1; } var computedTime = this.toSeconds(time); this.triggerAttack(notes, computedTime, velocity); if (isArray(duration)) { assert(isArray(notes), "notes must be an array when duration is array"); notes.forEach(function (note, index) { var d = duration[Math.min(index, duration.length - 1)]; _this.triggerRelease(note, computedTime + _this.toSeconds(d)); }); } else { this.triggerRelease(notes, computedTime + this.toSeconds(duration)); } return this; }; /** * Add a note to the sampler. * @param note The buffer's pitch. * @param url Either the url of the buffer, or a buffer which will be added with the given name. * @param callback The callback to invoke when the url is loaded. */ Sampler.prototype.add = function (note, url, callback) { assert(isNote(note) || isFinite(note), "note must be a pitch or midi: " + note); if (isNote(note)) { // convert the note name to MIDI var mid = new FrequencyClass(this.context, note).toMidi(); this._buffers.add(mid, url, callback); } else { // otherwise if it's numbers assume it's midi this._buffers.add(note, url, callback); } return this; }; Object.defineProperty(Sampler.prototype, "loaded", { /** * If the buffers are loaded or not */ get: function () { return this._buffers.loaded; }, enumerable: true, configurable: true }); /** * Clean up */ Sampler.prototype.dispose = function () { _super.prototype.dispose.call(this); this._buffers.dispose(); this._activeSources.forEach(function (sources) { sources.forEach(function (source) { return source.dispose(); }); }); this._activeSources.clear(); return this; }; tslib_1.__decorate([ timeRange(0) ], Sampler.prototype, "attack", void 0); tslib_1.__decorate([ timeRange(0) ], Sampler.prototype, "release", void 0); return Sampler; }(Instrument)); export { Sampler }; //# sourceMappingURL=Sampler.js.map