tone 15.2.6 → 15.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import { CompareToFile } from "../../test/helper/CompareToFile.js";
5
5
  import { InstrumentTest } from "../../test/helper/InstrumentTests.js";
6
6
  import { atTime, Offline } from "../../test/helper/Offline.js";
7
7
  import { ToneAudioBuffer } from "../core/context/ToneAudioBuffer.js";
8
+ import { getContext } from "../core/Global.js";
8
9
  import { Sampler } from "./Sampler.js";
9
10
 
10
11
  describe("Sampler", () => {
@@ -56,6 +57,7 @@ describe("Sampler", () => {
56
57
  {
57
58
  attack: 0.2,
58
59
  release: 0.3,
60
+ loop: true
59
61
  }
60
62
  );
61
63
  expect(sampler.attack).to.equal(0.2);
@@ -267,6 +269,97 @@ describe("Sampler", () => {
267
269
  });
268
270
  });
269
271
 
272
+ context("Looping", () => {
273
+
274
+ it("can be set to loop", () => {
275
+ const sampler = new Sampler();
276
+ sampler.loop = true;
277
+ expect(sampler.loop).to.be.true;
278
+ sampler.dispose();
279
+ });
280
+
281
+ it("can set the loop points", () => {
282
+ const sampler = new Sampler();
283
+ sampler.loopStart = 0.2;
284
+ expect(sampler.loopStart).to.equal(0.2);
285
+ sampler.loopEnd = 0.7;
286
+ expect(sampler.loopEnd).to.equal(0.7);
287
+ sampler.setLoopPoints(0, 0.5);
288
+ expect(sampler.loopStart).to.equal(0);
289
+ expect(sampler.loopEnd).to.equal(0.5);
290
+ sampler.dispose();
291
+ });
292
+
293
+ it("loops the audio", async () => {
294
+ const buff = await Offline(() => {
295
+ const sampler = new Sampler({
296
+ urls: {
297
+ A4: A4_buffer
298
+ }
299
+ });
300
+ sampler.loop = true;
301
+ sampler.toDestination();
302
+ sampler.triggerAttack("A4");
303
+ }, A4_buffer.duration * 1.5);
304
+ expect(buff.getRmsAtTime(0)).to.be.above(0);
305
+ expect(buff.getRmsAtTime(A4_buffer.duration * 0.5)).to.be.above(0);
306
+ expect(buff.getRmsAtTime(A4_buffer.duration)).to.be.above(0);
307
+ expect(buff.getRmsAtTime(A4_buffer.duration * 1.2)).to.be.above(0);
308
+ });
309
+
310
+ it("setting the loop multiple times has no affect", async () => {
311
+ const buff = await Offline(() => {
312
+ const sampler = new Sampler({
313
+ urls: {
314
+ A4: A4_buffer
315
+ }
316
+ });
317
+ sampler.loop = true;
318
+ sampler.loop = true;
319
+ sampler.toDestination();
320
+ sampler.triggerAttack("A4");
321
+ }, A4_buffer.duration * 1.5);
322
+ expect(buff.getRmsAtTime(0)).to.be.above(0);
323
+ expect(buff.getRmsAtTime(A4_buffer.duration * 0.5)).to.be.above(0);
324
+ expect(buff.getRmsAtTime(A4_buffer.duration)).to.be.above(0);
325
+ expect(buff.getRmsAtTime(A4_buffer.duration * 1.2)).to.be.above(0);
326
+ });
327
+
328
+ it("loops the audio when loop is set after start", async () => {
329
+ const buff = await Offline(() => {
330
+ const sampler = new Sampler({
331
+ urls: {
332
+ A4: A4_buffer
333
+ }
334
+ });
335
+ sampler.toDestination();
336
+ sampler.triggerAttack("A4");
337
+ sampler.loop = true;
338
+ }, A4_buffer.duration * 1.5);
339
+ expect(buff.getRmsAtTime(0)).to.be.above(0);
340
+ expect(buff.getRmsAtTime(A4_buffer.duration * 0.5)).to.be.above(0);
341
+ expect(buff.getRmsAtTime(A4_buffer.duration)).to.be.above(0);
342
+ expect(buff.getRmsAtTime(A4_buffer.duration * 1.2)).to.be.above(0);
343
+ });
344
+
345
+ it("starts buffers at loopStart when set to loop", async () => {
346
+ const testSample =
347
+ A4_buffer.toArray(0)[Math.floor(0.1 * getContext().sampleRate)];
348
+ const buff = await Offline(() => {
349
+ const sampler = new Sampler({
350
+ urls: {
351
+ A4: A4_buffer
352
+ }
353
+ });
354
+ sampler.loopStart = 0.1;
355
+ sampler.loop = true;
356
+ sampler.toDestination();
357
+ sampler.triggerAttack("A4");
358
+ }, 0.05);
359
+ expect(buff.toArray()[0][0]).to.equal(testSample);
360
+ });
361
+ });
362
+
270
363
  context("add samples", () => {
271
364
  it("can add a note with its midi value", async () => {
272
365
  const buffer = await Offline(() => {
@@ -10,9 +10,9 @@ import {
10
10
  Note,
11
11
  Time,
12
12
  } from "../core/type/Units.js";
13
- import { assert } from "../core/util/Debug.js";
13
+ import { assert, assertRange } from "../core/util/Debug.js";
14
14
  import { timeRange } from "../core/util/Decorator.js";
15
- import { optionsFromArguments } from "../core/util/Defaults.js";
15
+ import { defaultArg, optionsFromArguments } from "../core/util/Defaults.js";
16
16
  import { noOp } from "../core/util/Interface.js";
17
17
  import { isArray, isNote, isNumber } from "../core/util/TypeCheck.js";
18
18
  import { Instrument, InstrumentOptions } from "../instrument/Instrument.js";
@@ -34,6 +34,9 @@ export interface SamplerOptions extends InstrumentOptions {
34
34
  baseUrl: string;
35
35
  curve: ToneBufferSourceCurve;
36
36
  urls: SamplesMap;
37
+ loop: boolean;
38
+ loopEnd: number;
39
+ loopStart: number;
37
40
  }
38
41
 
39
42
  /**
@@ -70,6 +73,26 @@ export class Sampler extends Instrument<SamplerOptions> {
70
73
  */
71
74
  private _activeSources: Map<MidiNote, ToneBufferSource[]> = new Map();
72
75
 
76
+ /**
77
+ * The list of all provided midi notes
78
+ */
79
+ private _providedMidiNotes: MidiNote[] = [];
80
+
81
+ /**
82
+ * if the buffer should loop once its over
83
+ */
84
+ private _loop: boolean;
85
+
86
+ /**
87
+ * if 'loop' is true, the loop will start at this position
88
+ */
89
+ private _loopStart: Time;
90
+
91
+ /**
92
+ * if 'loop' is true, the loop will end at this position
93
+ */
94
+ private _loopEnd: Time;
95
+
73
96
  /**
74
97
  * The envelope applied to the beginning of the sample.
75
98
  * @min 0
@@ -144,6 +167,9 @@ export class Sampler extends Instrument<SamplerOptions> {
144
167
  this.attack = options.attack;
145
168
  this.release = options.release;
146
169
  this.curve = options.curve;
170
+ this._loop = options.loop;
171
+ this._loopStart = options.loopStart;
172
+ this._loopEnd = options.loopEnd;
147
173
 
148
174
  // invoke the callback if it's already loaded
149
175
  if (this._buffers.loaded) {
@@ -161,6 +187,9 @@ export class Sampler extends Instrument<SamplerOptions> {
161
187
  onerror: noOp,
162
188
  release: 0.1,
163
189
  urls: {},
190
+ loop: false,
191
+ loopEnd: 0,
192
+ loopStart: 0,
164
193
  });
165
194
  }
166
195
 
@@ -197,6 +226,7 @@ export class Sampler extends Instrument<SamplerOptions> {
197
226
  if (!Array.isArray(notes)) {
198
227
  notes = [notes];
199
228
  }
229
+ const offset = defaultArg(this._loopStart, 0);
200
230
  notes.forEach((note) => {
201
231
  const midiFloat = ftomf(
202
232
  new FrequencyClass(this.context, note).toFrequency()
@@ -210,6 +240,9 @@ export class Sampler extends Instrument<SamplerOptions> {
210
240
  const playbackRate = intervalToFrequencyRatio(
211
241
  difference + remainder
212
242
  );
243
+ const duration = this._loop
244
+ ? undefined
245
+ : buffer.duration / playbackRate;
213
246
  // play that note
214
247
  const source = new ToneBufferSource({
215
248
  url: buffer,
@@ -217,9 +250,12 @@ export class Sampler extends Instrument<SamplerOptions> {
217
250
  curve: this.curve,
218
251
  fadeIn: this.attack,
219
252
  fadeOut: this.release,
253
+ loop: this._loop,
254
+ loopStart: this._loopStart,
255
+ loopEnd: this._loopEnd,
220
256
  playbackRate,
221
257
  }).connect(this.output);
222
- source.start(time, 0, buffer.duration / playbackRate, velocity);
258
+ source.start(time, offset, duration, velocity);
223
259
  // add it to the active sources
224
260
  if (!isArray(this._activeSources.get(midi))) {
225
261
  this._activeSources.set(midi, []);
@@ -357,6 +393,98 @@ export class Sampler extends Instrument<SamplerOptions> {
357
393
  return this._buffers.loaded;
358
394
  }
359
395
 
396
+ /**
397
+ * Set the loop start and end. Will only loop if loop is set to true.
398
+ * @param loopStart The loop start time
399
+ * @param loopEnd The loop end time
400
+ * @example
401
+ * const sampler = new Tone.Sampler({
402
+ * urls: {
403
+ * A1: "https://tonejs.github.io/audio/berklee/guitar_chord4.mp3",
404
+ * },
405
+ * }).toDestination();
406
+ * // loop between the given points
407
+ * sampler.setLoopPoints(0.2, 0.3);
408
+ * sampler.loop = true;
409
+ */
410
+ setLoopPoints(loopStart: Time, loopEnd: Time): this {
411
+ this.loopStart = loopStart;
412
+ this.loopEnd = loopEnd;
413
+ return this;
414
+ }
415
+
416
+ /**
417
+ * If loop is true, the loop will start at this position.
418
+ */
419
+ get loopStart(): Time {
420
+ return this._loopStart;
421
+ }
422
+ set loopStart(loopStart) {
423
+ this._loopStart = loopStart;
424
+ this._providedMidiNotes.forEach((midiNote) => {
425
+ const buffer = this._buffers.get(midiNote);
426
+ if (buffer.loaded) {
427
+ assertRange(this.toSeconds(loopStart), 0, buffer.duration);
428
+ }
429
+ });
430
+ // get the current sources
431
+ this._activeSources.forEach((sourceList) => {
432
+ sourceList.forEach((source) => {
433
+ source.loopStart = loopStart;
434
+ });
435
+ });
436
+ }
437
+
438
+ /**
439
+ * If loop is true, the loop will end at this position.
440
+ */
441
+ get loopEnd(): Time {
442
+ return this._loopEnd;
443
+ }
444
+ set loopEnd(loopEnd) {
445
+ this._loopEnd = loopEnd;
446
+ this._providedMidiNotes.forEach((midiNote) => {
447
+ const buffer = this._buffers.get(midiNote);
448
+ if (buffer.loaded) {
449
+ assertRange(this.toSeconds(loopEnd), 0, buffer.duration);
450
+ }
451
+ });
452
+ // get the current sources
453
+ this._activeSources.forEach((sourceList) => {
454
+ sourceList.forEach((source) => {
455
+ source.loopEnd = loopEnd;
456
+ });
457
+ });
458
+ }
459
+
460
+
461
+ /**
462
+ * If the buffers should loop once they are over.
463
+ * @example
464
+ * const sampler = new Tone.Sampler({
465
+ * urls: {
466
+ * A4: "https://tonejs.github.io/audio/berklee/femalevoice_aa_A4.mp3",
467
+ * },
468
+ * }).toDestination();
469
+ * sampler.loop = true;
470
+ */
471
+ get loop(): boolean {
472
+ return this._loop;
473
+ }
474
+ set loop(loop) {
475
+ // if no change, do nothing
476
+ if (this._loop === loop) {
477
+ return;
478
+ }
479
+ this._loop = loop;
480
+ // set the loop of all of the sources
481
+ this._activeSources.forEach((sourceList) => {
482
+ sourceList.forEach((source) => {
483
+ source.loop = loop;
484
+ });
485
+ });
486
+ }
487
+
360
488
  /**
361
489
  * Clean up
362
490
  */
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.2.6";
1
+ export const version: string = "15.2.8";