tone 15.5.57 → 15.5.59

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.
@@ -88,6 +88,27 @@ describe("Sampler", () => {
88
88
  expect(buffer.isSilent()).to.be.false;
89
89
  });
90
90
 
91
+ it("get() reports back the urls, baseUrl, onload and onerror it was constructed with", () => {
92
+ const onload = () => {};
93
+ const onerror = () => {};
94
+ const sampler = new Sampler({
95
+ urls: {
96
+ 69: A4_buffer,
97
+ },
98
+ baseUrl: "./test/audio/",
99
+ onload,
100
+ onerror,
101
+ });
102
+ const options = sampler.get();
103
+ expect(options.urls).to.deep.equal({
104
+ 69: A4_buffer,
105
+ });
106
+ expect(options.baseUrl).to.equal("./test/audio/");
107
+ expect(options.onload).to.equal(onload);
108
+ expect(options.onerror).to.equal(onerror);
109
+ sampler.dispose();
110
+ });
111
+
91
112
  it("throws an error if there are no available notes to play", () => {
92
113
  expect(() => {
93
114
  const sampler = new Sampler();
@@ -34,9 +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
+ loop: boolean;
38
+ loopEnd: number;
39
+ loopStart: number;
40
40
  }
41
41
 
42
42
  /**
@@ -73,14 +73,14 @@ export class Sampler extends Instrument<SamplerOptions> {
73
73
  */
74
74
  private _activeSources: Map<MidiNote, ToneBufferSource[]> = new Map();
75
75
 
76
- /**
77
- * The list of all provided midi notes
78
- */
79
- private _providedMidiNotes: MidiNote[] = [];
76
+ /**
77
+ * The list of all provided midi notes
78
+ */
79
+ private _providedMidiNotes: MidiNote[] = [];
80
80
 
81
- /**
82
- * if the buffer should loop once its over
83
- */
81
+ /**
82
+ * if the buffer should loop once its over
83
+ */
84
84
  private _loop: boolean;
85
85
 
86
86
  /**
@@ -115,6 +115,26 @@ export class Sampler extends Instrument<SamplerOptions> {
115
115
  */
116
116
  curve: ToneBufferSourceCurve;
117
117
 
118
+ /**
119
+ * The urls passed in the constructor, kept so `get()` can report them back.
120
+ */
121
+ private _urls: SamplesMap;
122
+
123
+ /**
124
+ * The baseUrl passed in the constructor, kept so `get()` can report it back.
125
+ */
126
+ private _baseUrl: string;
127
+
128
+ /**
129
+ * The onload callback passed in the constructor, kept so `get()` can report it back.
130
+ */
131
+ private _onload: () => void;
132
+
133
+ /**
134
+ * The onerror callback passed in the constructor, kept so `get()` can report it back.
135
+ */
136
+ private _onerror: (error: Error) => void;
137
+
118
138
  /**
119
139
  * @param samples An object of samples mapping either Midi Note Numbers or
120
140
  * Scientific Pitch Notation to the url of that sample.
@@ -167,9 +187,13 @@ export class Sampler extends Instrument<SamplerOptions> {
167
187
  this.attack = options.attack;
168
188
  this.release = options.release;
169
189
  this.curve = options.curve;
170
- this._loop = options.loop;
171
- this._loopStart = options.loopStart;
172
- this._loopEnd = options.loopEnd;
190
+ this._urls = options.urls;
191
+ this._baseUrl = options.baseUrl;
192
+ this._onload = options.onload;
193
+ this._onerror = options.onerror;
194
+ this._loop = options.loop;
195
+ this._loopStart = options.loopStart;
196
+ this._loopEnd = options.loopEnd;
173
197
 
174
198
  // invoke the callback if it's already loaded
175
199
  if (this._buffers.loaded) {
@@ -187,9 +211,23 @@ export class Sampler extends Instrument<SamplerOptions> {
187
211
  onerror: noOp,
188
212
  release: 0.1,
189
213
  urls: {},
190
- loop: false,
191
- loopEnd: 0,
192
- loopStart: 0,
214
+ loop: false,
215
+ loopEnd: 0,
216
+ loopStart: 0,
217
+ });
218
+ }
219
+
220
+ /**
221
+ * urls, baseUrl, onload and onerror aren't stored as regular class
222
+ * properties (unlike attack/release/curve), so the base implementation
223
+ * can't find them via `Reflect.has` and they'd be silently missing here.
224
+ */
225
+ get(): SamplerOptions {
226
+ return Object.assign(super.get(), {
227
+ urls: this._urls,
228
+ baseUrl: this._baseUrl,
229
+ onload: this._onload,
230
+ onerror: this._onerror,
193
231
  });
194
232
  }
195
233
 
@@ -226,7 +264,7 @@ export class Sampler extends Instrument<SamplerOptions> {
226
264
  if (!Array.isArray(notes)) {
227
265
  notes = [notes];
228
266
  }
229
- const offset = defaultArg(this._loopStart, 0);
267
+ const offset = defaultArg(this._loopStart, 0);
230
268
  notes.forEach((note) => {
231
269
  const midiFloat = ftomf(
232
270
  new FrequencyClass(this.context, note).toFrequency()
@@ -240,9 +278,9 @@ export class Sampler extends Instrument<SamplerOptions> {
240
278
  const playbackRate = intervalToFrequencyRatio(
241
279
  difference + remainder
242
280
  );
243
- const duration = this._loop
244
- ? undefined
245
- : buffer.duration / playbackRate;
281
+ const duration = this._loop
282
+ ? undefined
283
+ : buffer.duration / playbackRate;
246
284
  // play that note
247
285
  const source = new ToneBufferSource({
248
286
  url: buffer,
@@ -250,9 +288,9 @@ export class Sampler extends Instrument<SamplerOptions> {
250
288
  curve: this.curve,
251
289
  fadeIn: this.attack,
252
290
  fadeOut: this.release,
253
- loop: this._loop,
254
- loopStart: this._loopStart,
255
- loopEnd: this._loopEnd,
291
+ loop: this._loop,
292
+ loopStart: this._loopStart,
293
+ loopEnd: this._loopEnd,
256
294
  playbackRate,
257
295
  }).connect(this.output);
258
296
  source.start(time, offset, duration, velocity);
@@ -393,98 +431,97 @@ export class Sampler extends Instrument<SamplerOptions> {
393
431
  return this._buffers.loaded;
394
432
  }
395
433
 
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
-
434
+ /**
435
+ * Set the loop start and end. Will only loop if loop is set to true.
436
+ * @param loopStart The loop start time
437
+ * @param loopEnd The loop end time
438
+ * @example
439
+ * const sampler = new Tone.Sampler({
440
+ * urls: {
441
+ * A1: "https://tonejs.github.io/audio/berklee/guitar_chord4.mp3",
442
+ * },
443
+ * }).toDestination();
444
+ * // loop between the given points
445
+ * sampler.setLoopPoints(0.2, 0.3);
446
+ * sampler.loop = true;
447
+ */
448
+ setLoopPoints(loopStart: Time, loopEnd: Time): this {
449
+ this.loopStart = loopStart;
450
+ this.loopEnd = loopEnd;
451
+ return this;
452
+ }
453
+
454
+ /**
455
+ * If loop is true, the loop will start at this position.
456
+ */
457
+ get loopStart(): Time {
458
+ return this._loopStart;
459
+ }
460
+ set loopStart(loopStart) {
461
+ this._loopStart = loopStart;
462
+ this._providedMidiNotes.forEach((midiNote) => {
463
+ const buffer = this._buffers.get(midiNote);
464
+ if (buffer.loaded) {
465
+ assertRange(this.toSeconds(loopStart), 0, buffer.duration);
466
+ }
467
+ });
468
+ // get the current sources
469
+ this._activeSources.forEach((sourceList) => {
470
+ sourceList.forEach((source) => {
471
+ source.loopStart = loopStart;
472
+ });
473
+ });
474
+ }
475
+
476
+ /**
477
+ * If loop is true, the loop will end at this position.
478
+ */
479
+ get loopEnd(): Time {
480
+ return this._loopEnd;
481
+ }
482
+ set loopEnd(loopEnd) {
483
+ this._loopEnd = loopEnd;
484
+ this._providedMidiNotes.forEach((midiNote) => {
485
+ const buffer = this._buffers.get(midiNote);
486
+ if (buffer.loaded) {
487
+ assertRange(this.toSeconds(loopEnd), 0, buffer.duration);
488
+ }
489
+ });
490
+ // get the current sources
491
+ this._activeSources.forEach((sourceList) => {
492
+ sourceList.forEach((source) => {
493
+ source.loopEnd = loopEnd;
494
+ });
495
+ });
496
+ }
497
+
498
+ /**
499
+ * If the buffers should loop once they are over.
500
+ * @example
501
+ * const sampler = new Tone.Sampler({
502
+ * urls: {
503
+ * A4: "https://tonejs.github.io/audio/berklee/femalevoice_aa_A4.mp3",
504
+ * },
505
+ * }).toDestination();
506
+ * sampler.loop = true;
507
+ */
508
+ get loop(): boolean {
509
+ return this._loop;
510
+ }
511
+ set loop(loop) {
512
+ // if no change, do nothing
513
+ if (this._loop === loop) {
514
+ return;
515
+ }
516
+ this._loop = loop;
517
+ // set the loop of all of the sources
518
+ this._activeSources.forEach((sourceList) => {
519
+ sourceList.forEach((source) => {
520
+ source.loop = loop;
521
+ });
522
+ });
523
+ }
524
+
488
525
  /**
489
526
  * Clean up
490
527
  */
@@ -38,30 +38,30 @@ describe("ToneConstantSource", () => {
38
38
  });
39
39
 
40
40
  context("onended", () => {
41
- it("invokes the onended callback in the online context", (done) => {
41
+ it("invokes the onended callback in the online context", async () => {
42
42
  const source = new ToneConstantSource();
43
43
  source.start();
44
44
  source.stop("+0.3");
45
45
  const now = source.now();
46
- source.onended = () => {
47
- expect(source.now() - now).to.be.closeTo(0.3, 0.15);
48
- source.dispose();
49
- done();
50
- };
46
+ await new Promise<void>((resolve) => {
47
+ source.onended = () => resolve();
48
+ });
49
+ expect(source.now() - now).to.be.within(0.25, 0.6);
50
+ source.dispose();
51
51
  });
52
52
 
53
- it("invokes the onended callback only once in the online context", (done) => {
53
+ it("invokes the onended callback only once in the online context", async () => {
54
54
  const source = new ToneConstantSource();
55
55
  source.start();
56
56
  source.stop("+0.1");
57
57
  source.stop("+0.2");
58
58
  source.stop("+0.3");
59
59
  const now = source.now();
60
- source.onended = () => {
61
- expect(source.now() - now).to.be.within(0.25, 0.5);
62
- source.dispose();
63
- done();
64
- };
60
+ await new Promise<void>((resolve) => {
61
+ source.onended = () => resolve();
62
+ });
63
+ expect(source.now() - now).to.be.within(0.25, 0.6);
64
+ source.dispose();
65
65
  });
66
66
 
67
67
  it("invokes the onended callback in the offline context", async () => {
@@ -129,6 +129,17 @@ describe("ToneConstantSource", () => {
129
129
  expect(buffer.getValueAtTime(0.2)).to.equal(0);
130
130
  });
131
131
 
132
+ it("stops the native ConstantSourceNode once stop() takes effect", async () => {
133
+ const stopSpy = sinon.spy(ConstantSourceNode.prototype, "stop");
134
+ const source = new ToneConstantSource().start();
135
+ source.stop();
136
+ await new Promise<void>((resolve) => {
137
+ source.onended = () => resolve();
138
+ });
139
+ expect(stopSpy).to.have.been.calledOnce;
140
+ source.dispose();
141
+ });
142
+
132
143
  it("clamps start time to the currentTime", () => {
133
144
  const source = new ToneConstantSource();
134
145
  source.start(0);
@@ -31,6 +31,11 @@ export class ToneConstantSource<
31
31
  */
32
32
  private _source?: ConstantSourceNode;
33
33
 
34
+ /**
35
+ * Whether the native source was started, so it can be stopped
36
+ */
37
+ private _sourceStarted = false;
38
+
34
39
  /**
35
40
  * The offset of the signal generator
36
41
  */
@@ -86,6 +91,7 @@ export class ToneConstantSource<
86
91
  this._stopTime !== -1 && this._stopTime < this._startTime;
87
92
  if (isStarted && !isStoppedBeforeStart) {
88
93
  this._source.start(this._startTime);
94
+ this._sourceStarted = true;
89
95
  }
90
96
  }
91
97
 
@@ -97,15 +103,19 @@ export class ToneConstantSource<
97
103
  const computedTime = this.toSeconds(time);
98
104
  this.log("start", computedTime);
99
105
  this._startGain(computedTime);
100
- this._source?.start(computedTime);
106
+ if (this._source) {
107
+ this._source.start(computedTime);
108
+ this._sourceStarted = true;
109
+ }
101
110
  return this;
102
111
  }
103
112
 
104
113
  protected _stopSource(time?: Seconds): void {
105
- if (this.state === "stopped") {
106
- return;
114
+ // state is already "stopped" here: OneShotSource defers this call
115
+ // until after _stopTime, so check the native node instead
116
+ if (this._sourceStarted) {
117
+ this._source?.stop(time);
107
118
  }
108
- this._source?.stop(time);
109
119
  }
110
120
 
111
121
  dispose(): this {
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.5.57";
1
+ export const version: string = "15.5.59";