UNPKG

9.62 kBJavaScriptView Raw
1import { __decorate } from "tslib";
2import { ToneAudioBuffers } from "../core/context/ToneAudioBuffers.js";
3import { ftomf, intervalToFrequencyRatio } from "../core/type/Conversions.js";
4import { FrequencyClass } from "../core/type/Frequency.js";
5import { optionsFromArguments } from "../core/util/Defaults.js";
6import { noOp } from "../core/util/Interface.js";
7import { isArray, isNote, isNumber } from "../core/util/TypeCheck.js";
8import { Instrument } from "../instrument/Instrument.js";
9import { ToneBufferSource, } from "../source/buffer/ToneBufferSource.js";
10import { timeRange } from "../core/util/Decorator.js";
11import { assert } from "../core/util/Debug.js";
12/**
13 * Pass in an object which maps the note's pitch or midi value to the url,
14 * then you can trigger the attack and release of that note like other instruments.
15 * By automatically repitching the samples, it is possible to play pitches which
16 * were not explicitly included which can save loading time.
17 *
18 * For sample or buffer playback where repitching is not necessary,
19 * use {@link Player}.
20 * @example
21 * const sampler = new Tone.Sampler({
22 * urls: {
23 * A1: "A1.mp3",
24 * A2: "A2.mp3",
25 * },
26 * baseUrl: "https://tonejs.github.io/audio/casio/",
27 * onload: () => {
28 * sampler.triggerAttackRelease(["C1", "E1", "G1", "B1"], 0.5);
29 * }
30 * }).toDestination();
31 * @category Instrument
32 */
33export class Sampler extends Instrument {
34 constructor() {
35 const options = optionsFromArguments(Sampler.getDefaults(), arguments, ["urls", "onload", "baseUrl"], "urls");
36 super(options);
37 this.name = "Sampler";
38 /**
39 * The object of all currently playing BufferSources
40 */
41 this._activeSources = new Map();
42 const urlMap = {};
43 Object.keys(options.urls).forEach((note) => {
44 const noteNumber = parseInt(note, 10);
45 assert(isNote(note) || (isNumber(noteNumber) && isFinite(noteNumber)), `url key is neither a note or midi pitch: ${note}`);
46 if (isNote(note)) {
47 // convert the note name to MIDI
48 const mid = new FrequencyClass(this.context, note).toMidi();
49 urlMap[mid] = options.urls[note];
50 }
51 else if (isNumber(noteNumber) && isFinite(noteNumber)) {
52 // otherwise if it's numbers assume it's midi
53 urlMap[noteNumber] = options.urls[noteNumber];
54 }
55 });
56 this._buffers = new ToneAudioBuffers({
57 urls: urlMap,
58 onload: options.onload,
59 baseUrl: options.baseUrl,
60 onerror: options.onerror,
61 });
62 this.attack = options.attack;
63 this.release = options.release;
64 this.curve = options.curve;
65 // invoke the callback if it's already loaded
66 if (this._buffers.loaded) {
67 // invoke onload deferred
68 Promise.resolve().then(options.onload);
69 }
70 }
71 static getDefaults() {
72 return Object.assign(Instrument.getDefaults(), {
73 attack: 0,
74 baseUrl: "",
75 curve: "exponential",
76 onload: noOp,
77 onerror: noOp,
78 release: 0.1,
79 urls: {},
80 });
81 }
82 /**
83 * Returns the difference in steps between the given midi note at the closets sample.
84 */
85 _findClosest(midi) {
86 // searches within 8 octaves of the given midi note
87 const MAX_INTERVAL = 96;
88 let interval = 0;
89 while (interval < MAX_INTERVAL) {
90 // check above and below
91 if (this._buffers.has(midi + interval)) {
92 return -interval;
93 }
94 else if (this._buffers.has(midi - interval)) {
95 return interval;
96 }
97 interval++;
98 }
99 throw new Error(`No available buffers for note: ${midi}`);
100 }
101 /**
102 * @param notes The note to play, or an array of notes.
103 * @param time When to play the note
104 * @param velocity The velocity to play the sample back.
105 */
106 triggerAttack(notes, time, velocity = 1) {
107 this.log("triggerAttack", notes, time, velocity);
108 if (!Array.isArray(notes)) {
109 notes = [notes];
110 }
111 notes.forEach((note) => {
112 const midiFloat = ftomf(new FrequencyClass(this.context, note).toFrequency());
113 const midi = Math.round(midiFloat);
114 const remainder = midiFloat - midi;
115 // find the closest note pitch
116 const difference = this._findClosest(midi);
117 const closestNote = midi - difference;
118 const buffer = this._buffers.get(closestNote);
119 const playbackRate = intervalToFrequencyRatio(difference + remainder);
120 // play that note
121 const source = new ToneBufferSource({
122 url: buffer,
123 context: this.context,
124 curve: this.curve,
125 fadeIn: this.attack,
126 fadeOut: this.release,
127 playbackRate,
128 }).connect(this.output);
129 source.start(time, 0, buffer.duration / playbackRate, velocity);
130 // add it to the active sources
131 if (!isArray(this._activeSources.get(midi))) {
132 this._activeSources.set(midi, []);
133 }
134 this._activeSources.get(midi).push(source);
135 // remove it when it's done
136 source.onended = () => {
137 if (this._activeSources && this._activeSources.has(midi)) {
138 const sources = this._activeSources.get(midi);
139 const index = sources.indexOf(source);
140 if (index !== -1) {
141 sources.splice(index, 1);
142 }
143 }
144 };
145 });
146 return this;
147 }
148 /**
149 * @param notes The note to release, or an array of notes.
150 * @param time When to release the note.
151 */
152 triggerRelease(notes, time) {
153 this.log("triggerRelease", notes, time);
154 if (!Array.isArray(notes)) {
155 notes = [notes];
156 }
157 notes.forEach((note) => {
158 const midi = new FrequencyClass(this.context, note).toMidi();
159 // find the note
160 if (this._activeSources.has(midi) &&
161 this._activeSources.get(midi).length) {
162 const sources = this._activeSources.get(midi);
163 time = this.toSeconds(time);
164 sources.forEach((source) => {
165 source.stop(time);
166 });
167 this._activeSources.set(midi, []);
168 }
169 });
170 return this;
171 }
172 /**
173 * Release all currently active notes.
174 * @param time When to release the notes.
175 */
176 releaseAll(time) {
177 const computedTime = this.toSeconds(time);
178 this._activeSources.forEach((sources) => {
179 while (sources.length) {
180 const source = sources.shift();
181 source.stop(computedTime);
182 }
183 });
184 return this;
185 }
186 sync() {
187 if (this._syncState()) {
188 this._syncMethod("triggerAttack", 1);
189 this._syncMethod("triggerRelease", 1);
190 }
191 return this;
192 }
193 /**
194 * Invoke the attack phase, then after the duration, invoke the release.
195 * @param notes The note to play and release, or an array of notes.
196 * @param duration The time the note should be held
197 * @param time When to start the attack
198 * @param velocity The velocity of the attack
199 */
200 triggerAttackRelease(notes, duration, time, velocity = 1) {
201 const computedTime = this.toSeconds(time);
202 this.triggerAttack(notes, computedTime, velocity);
203 if (isArray(duration)) {
204 assert(isArray(notes), "notes must be an array when duration is array");
205 notes.forEach((note, index) => {
206 const d = duration[Math.min(index, duration.length - 1)];
207 this.triggerRelease(note, computedTime + this.toSeconds(d));
208 });
209 }
210 else {
211 this.triggerRelease(notes, computedTime + this.toSeconds(duration));
212 }
213 return this;
214 }
215 /**
216 * Add a note to the sampler.
217 * @param note The buffer's pitch.
218 * @param url Either the url of the buffer, or a buffer which will be added with the given name.
219 * @param callback The callback to invoke when the url is loaded.
220 */
221 add(note, url, callback) {
222 assert(isNote(note) || isFinite(note), `note must be a pitch or midi: ${note}`);
223 if (isNote(note)) {
224 // convert the note name to MIDI
225 const mid = new FrequencyClass(this.context, note).toMidi();
226 this._buffers.add(mid, url, callback);
227 }
228 else {
229 // otherwise if it's numbers assume it's midi
230 this._buffers.add(note, url, callback);
231 }
232 return this;
233 }
234 /**
235 * If the buffers are loaded or not
236 */
237 get loaded() {
238 return this._buffers.loaded;
239 }
240 /**
241 * Clean up
242 */
243 dispose() {
244 super.dispose();
245 this._buffers.dispose();
246 this._activeSources.forEach((sources) => {
247 sources.forEach((source) => source.dispose());
248 });
249 this._activeSources.clear();
250 return this;
251 }
252}
253__decorate([
254 timeRange(0)
255], Sampler.prototype, "attack", void 0);
256__decorate([
257 timeRange(0)
258], Sampler.prototype, "release", void 0);
259//# sourceMappingURL=Sampler.js.map
\No newline at end of file