tone 15.5.47 → 15.5.49

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.
Files changed (55) hide show
  1. package/Tone/core/clock/Clock.test.ts +29 -0
  2. package/Tone/core/clock/TickParam.ts +1 -1
  3. package/Tone/core/clock/TickSource.test.ts +13 -0
  4. package/Tone/core/clock/TickSource.ts +2 -3
  5. package/Tone/core/clock/Ticker.ts +1 -1
  6. package/Tone/core/context/Param.ts +1 -1
  7. package/Tone/core/context/ToneAudioBuffer.ts +5 -25
  8. package/Tone/core/util/StateTimeline.ts +1 -5
  9. package/Tone/core/util/Timeline.test.ts +56 -0
  10. package/Tone/core/util/Timeline.ts +22 -10
  11. package/Tone/core/util/TimelineValue.ts +1 -5
  12. package/Tone/core/worklet/SingleIOProcessor.worklet.ts +2 -2
  13. package/Tone/core/worklet/ToneAudioWorklet.ts +2 -4
  14. package/Tone/fromContext.ts +2 -2
  15. package/Tone/instrument/Sampler.ts +1 -1
  16. package/Tone/source/Noise.test.ts +6 -0
  17. package/Tone/source/Noise.ts +1 -3
  18. package/Tone/source/Source.test.ts +31 -0
  19. package/Tone/source/UserMedia.ts +4 -16
  20. package/Tone/source/buffer/Player.ts +1 -1
  21. package/Tone/version.ts +1 -1
  22. package/build/Tone.js +1 -1
  23. package/build/Tone.js.map +1 -1
  24. package/build/esm/core/clock/TickParam.js +1 -1
  25. package/build/esm/core/clock/TickParam.js.map +1 -1
  26. package/build/esm/core/clock/TickSource.js +2 -3
  27. package/build/esm/core/clock/TickSource.js.map +1 -1
  28. package/build/esm/core/clock/Ticker.js +2 -1
  29. package/build/esm/core/clock/Ticker.js.map +1 -1
  30. package/build/esm/core/context/Param.js +1 -1
  31. package/build/esm/core/context/Param.js.map +1 -1
  32. package/build/esm/core/context/ToneAudioBuffer.js +10 -30
  33. package/build/esm/core/context/ToneAudioBuffer.js.map +1 -1
  34. package/build/esm/core/util/StateTimeline.js +2 -6
  35. package/build/esm/core/util/StateTimeline.js.map +1 -1
  36. package/build/esm/core/util/Timeline.js +19 -10
  37. package/build/esm/core/util/Timeline.js.map +1 -1
  38. package/build/esm/core/util/TimelineValue.js +2 -6
  39. package/build/esm/core/util/TimelineValue.js.map +1 -1
  40. package/build/esm/core/worklet/SingleIOProcessor.worklet.d.ts +1 -1
  41. package/build/esm/core/worklet/SingleIOProcessor.worklet.js +2 -2
  42. package/build/esm/core/worklet/ToneAudioWorklet.js +3 -4
  43. package/build/esm/core/worklet/ToneAudioWorklet.js.map +1 -1
  44. package/build/esm/fromContext.js +2 -2
  45. package/build/esm/fromContext.js.map +1 -1
  46. package/build/esm/instrument/Sampler.js +2 -1
  47. package/build/esm/instrument/Sampler.js.map +1 -1
  48. package/build/esm/source/Noise.js +2 -3
  49. package/build/esm/source/Noise.js.map +1 -1
  50. package/build/esm/source/UserMedia.js +8 -19
  51. package/build/esm/source/UserMedia.js.map +1 -1
  52. package/build/esm/source/buffer/Player.js +1 -1
  53. package/build/esm/source/buffer/Player.js.map +1 -1
  54. package/build/esm/version.js +1 -1
  55. package/package.json +1 -1
@@ -413,6 +413,35 @@ describe("Clock", () => {
413
413
  // the post-restart ticks from offset 40
414
414
  expect(observedTicks).to.deep.equal([0, 1, 2, 40, 41]);
415
415
  });
416
+
417
+ it("emits the stop event when stop and the following start happen at the same time within the stale window", async () => {
418
+ const events: Array<[string, number]> = [];
419
+ let restartTime = 0;
420
+ await Offline(() => {
421
+ const clock = new Clock(noOp, 200);
422
+ const boundLoop = (
423
+ clock as unknown as { _boundLoop: () => void }
424
+ )._boundLoop;
425
+ clock.on("stop", (time) => events.push(["stop", time]));
426
+ clock.on("start", (time) => events.push(["start", time]));
427
+ clock.start(0);
428
+ // Same staleness simulation as above. stop() and start() are
429
+ // scheduled at the same instant, as happens when both default
430
+ // to now() within one task: `transport.stop(); transport.start()`
431
+ clock.context.off("tick", boundLoop);
432
+ return atTime(0.02, (time) => {
433
+ restartTime = time;
434
+ clock.stop(time);
435
+ clock.start(time, 40);
436
+ clock.context.on("tick", boundLoop);
437
+ });
438
+ }, 0.03);
439
+ expect(events).to.deep.equal([
440
+ ["start", 0],
441
+ ["stop", restartTime],
442
+ ["start", restartTime],
443
+ ]);
444
+ });
416
445
  });
417
446
 
418
447
  context("Events", () => {
@@ -217,7 +217,7 @@ export class TickParam<
217
217
  getTimeOfTick(tick: Ticks): Seconds {
218
218
  const before = this._events.get(tick, "ticks");
219
219
  const after = this._events.getAfter(tick, "ticks");
220
- if (before && before.ticks === tick) {
220
+ if (before?.ticks === tick) {
221
221
  return before.time;
222
222
  } else if (
223
223
  before &&
@@ -76,6 +76,19 @@ describe("TickSource", () => {
76
76
  }, 0.5);
77
77
  });
78
78
 
79
+ it("reports the restart offset just after a stop and start at the same time", () => {
80
+ const source = new TickSource(3);
81
+ source.start(0);
82
+ source.stop(1);
83
+ source.start(1, 10);
84
+ expect(source.getTicksAtTime(1)).to.be.closeTo(10, 0.01);
85
+ // the first tick time of the restarted period is computed from the
86
+ // frequency integral and can land a rounding error after the start
87
+ expect(source.getTicksAtTime(1 + 1e-12)).to.be.closeTo(10, 0.01);
88
+ expect(source.getTicksAtTime(1.5)).to.be.closeTo(11.5, 0.01);
89
+ source.dispose();
90
+ });
91
+
79
92
  it("ticks restart at 0 when started after stop", () => {
80
93
  const source = new TickSource(3);
81
94
  source.start(0).stop(1).start(2);
@@ -398,8 +398,7 @@ export class TickSource<
398
398
  let lastStateEvent = this._state.get(startTime);
399
399
  this._state.forEachBetween(startTime, endTime, (event) => {
400
400
  if (
401
- lastStateEvent &&
402
- lastStateEvent.state === "started" &&
401
+ lastStateEvent?.state === "started" &&
403
402
  event.state !== "started"
404
403
  ) {
405
404
  this.forEachTickBetween(
@@ -413,7 +412,7 @@ export class TickSource<
413
412
 
414
413
  let error: Error | null = null;
415
414
 
416
- if (lastStateEvent && lastStateEvent.state === "started") {
415
+ if (lastStateEvent?.state === "started") {
417
416
  const maxStartTime = Math.max(lastStateEvent.time, startTime);
418
417
  // Figure out how far past the last whole-tick boundary maxStartTime
419
418
  // sits, so we can compute the time of the next tick at or after it.
@@ -123,8 +123,8 @@ export class Ticker {
123
123
  if (this._timeout) {
124
124
  clearTimeout(this._timeout);
125
125
  }
126
+ this._worker?.terminate();
126
127
  if (this._worker) {
127
- this._worker.terminate();
128
128
  this._worker.onmessage = null;
129
129
  }
130
130
  }
@@ -618,7 +618,7 @@ export class Param<TypeName extends UnitName = "number">
618
618
  param.setValueAtTime(this.getValueAtTime(now) as number, now);
619
619
  // if the previous event was a curve, then set the rest of it
620
620
  const previousEvent = this._events.get(now);
621
- if (previousEvent && previousEvent.type === "setTargetAtTime") {
621
+ if (previousEvent?.type === "setTargetAtTime") {
622
622
  // approx it until the next event with linear ramps
623
623
  const nextEvent = this._events.getAfter(previousEvent.time);
624
624
  // or for 2 seconds if there is no event
@@ -87,11 +87,7 @@ export class ToneAudioBuffer extends Tone {
87
87
  * The sample rate of the AudioBuffer
88
88
  */
89
89
  get sampleRate(): number {
90
- if (this._buffer) {
91
- return this._buffer.sampleRate;
92
- } else {
93
- return getContext().sampleRate;
94
- }
90
+ return this._buffer?.sampleRate ?? getContext().sampleRate;
95
91
  }
96
92
 
97
93
  /**
@@ -233,11 +229,7 @@ export class ToneAudioBuffer extends Tone {
233
229
  * @return The audio as a TypedArray
234
230
  */
235
231
  getChannelData(channel: number): Float32Array {
236
- if (this._buffer) {
237
- return this._buffer.getChannelData(channel);
238
- } else {
239
- return new Float32Array(0);
240
- }
232
+ return this._buffer?.getChannelData(channel) ?? new Float32Array(0);
241
233
  }
242
234
 
243
235
  /**
@@ -292,33 +284,21 @@ export class ToneAudioBuffer extends Tone {
292
284
  * The duration of the buffer in seconds.
293
285
  */
294
286
  get duration(): Seconds {
295
- if (this._buffer) {
296
- return this._buffer.duration;
297
- } else {
298
- return 0;
299
- }
287
+ return this._buffer?.duration ?? 0;
300
288
  }
301
289
 
302
290
  /**
303
291
  * The length of the buffer in samples
304
292
  */
305
293
  get length(): Samples {
306
- if (this._buffer) {
307
- return this._buffer.length;
308
- } else {
309
- return 0;
310
- }
294
+ return this._buffer?.length ?? 0;
311
295
  }
312
296
 
313
297
  /**
314
298
  * The number of discrete audio channels. Returns 0 if no buffer is loaded.
315
299
  */
316
300
  get numberOfChannels(): number {
317
- if (this._buffer) {
318
- return this._buffer.numberOfChannels;
319
- } else {
320
- return 0;
321
- }
301
+ return this._buffer?.numberOfChannels ?? 0;
322
302
  }
323
303
 
324
304
  /**
@@ -38,11 +38,7 @@ export class StateTimeline<
38
38
  */
39
39
  getValueAtTime(time: Seconds): PlaybackState {
40
40
  const event = this.get(time);
41
- if (event !== null) {
42
- return event.state;
43
- } else {
44
- return this._initial;
45
- }
41
+ return event?.state ?? this._initial;
46
42
  }
47
43
 
48
44
  /**
@@ -604,5 +604,61 @@ describe("Timeline", () => {
604
604
  expect(count).to.equal(0);
605
605
  sched.dispose();
606
606
  });
607
+
608
+ it("includes every event at the start time and excludes every event at the end time", () => {
609
+ const sched = new Timeline<{ time: number; id: string }>();
610
+ sched.add({ time: 0.1, id: "a" });
611
+ sched.add({ time: 0.2, id: "b" });
612
+ sched.add({ time: 0.2, id: "c" });
613
+ sched.add({ time: 0.3, id: "d" });
614
+ sched.add({ time: 0.3, id: "e" });
615
+ const visited = (start: number, end: number): string[] => {
616
+ const ids: string[] = [];
617
+ sched.forEachBetween(start, end, (event) => ids.push(event.id));
618
+ return ids;
619
+ };
620
+ expect(visited(0.1, 0.2)).to.deep.equal(["a"]);
621
+ expect(visited(0.2, 0.3)).to.deep.equal(["b", "c"]);
622
+ expect(visited(0.3, 0.4)).to.deep.equal(["d", "e"]);
623
+ expect(visited(0.15, 0.25)).to.deep.equal(["b", "c"]);
624
+ sched.dispose();
625
+ });
626
+
627
+ it("visits an event added at the boundary after the previous range was iterated", () => {
628
+ // two consecutive ranges [0.1, 0.2) and [0.2, 0.4); an event at 0.2
629
+ // is added between the two iterations, after another event at 0.2
630
+ const sched = new Timeline<{ time: number; id: string }>();
631
+ sched.add({ time: 0.1, id: "a" });
632
+ sched.add({ time: 0.2, id: "b" });
633
+ const ids: string[] = [];
634
+ sched.forEachBetween(0.1, 0.2, (event) => ids.push(event.id));
635
+ sched.add({ time: 0.2, id: "c" });
636
+ sched.forEachBetween(0.2, 0.4, (event) => ids.push(event.id));
637
+ expect(ids).to.deep.equal(["a", "b", "c"]);
638
+ sched.dispose();
639
+ });
640
+
641
+ it("includes events within rounding error of the start time", () => {
642
+ // _search compares times with an epsilon; the range bounds must too
643
+ const sched = new Timeline<{ time: number; id: string }>();
644
+ sched.add({ time: 0.1, id: "a" });
645
+ sched.add({ time: 0.2, id: "b" });
646
+ sched.add({ time: 0.2 + 1e-12, id: "c" });
647
+ const ids: string[] = [];
648
+ sched.forEachBetween(0.2, 0.3, (event) => ids.push(event.id));
649
+ expect(ids).to.deep.equal(["b", "c"]);
650
+ sched.dispose();
651
+ });
652
+
653
+ it("excludes the end time when the start time is before the first event", () => {
654
+ const sched = new Timeline<{ time: number; id: string }>();
655
+ sched.add({ time: 0.1, id: "a" });
656
+ sched.add({ time: 0.2, id: "b" });
657
+ sched.add({ time: 0.2, id: "c" });
658
+ const ids: string[] = [];
659
+ sched.forEachBetween(0, 0.2, (event) => ids.push(event.id));
660
+ expect(ids).to.deep.equal(["a"]);
661
+ sched.dispose();
662
+ });
607
663
  });
608
664
  });
@@ -387,20 +387,32 @@ export class Timeline<GenericEvent extends TimelineEvent> extends Tone {
387
387
  endTime: number,
388
388
  callback: (event: GenericEvent) => void
389
389
  ): this {
390
+ // _search compares times with an epsilon, so do the same here
390
391
  let lowerBound = this._search(startTime);
391
392
  let upperBound = this._search(endTime);
392
- if (lowerBound !== -1 && upperBound !== -1) {
393
- if (this._timeline[lowerBound].time !== startTime) {
394
- lowerBound += 1;
393
+ if (lowerBound === -1) {
394
+ // every event is after the start time
395
+ lowerBound = 0;
396
+ } else if (EQ(this._timeline[lowerBound].time, startTime)) {
397
+ // _search returns the last event at the start time,
398
+ // include every event at the start time
399
+ while (
400
+ lowerBound > 0 &&
401
+ EQ(this._timeline[lowerBound - 1].time, startTime)
402
+ ) {
403
+ lowerBound -= 1;
395
404
  }
396
- // exclusive of the end time
397
- if (this._timeline[upperBound].time === endTime) {
398
- upperBound -= 1;
399
- }
400
- this._iterate(callback, lowerBound, upperBound);
401
- } else if (lowerBound === -1) {
402
- this._iterate(callback, 0, upperBound);
405
+ } else {
406
+ lowerBound += 1;
407
+ }
408
+ // exclusive of the end time
409
+ while (
410
+ upperBound >= 0 &&
411
+ EQ(this._timeline[upperBound].time, endTime)
412
+ ) {
413
+ upperBound -= 1;
403
414
  }
415
+ this._iterate(callback, lowerBound, upperBound);
404
416
  return this;
405
417
  }
406
418
 
@@ -48,10 +48,6 @@ export class TimelineValue<Type> extends Tone {
48
48
  */
49
49
  get(time: Seconds): Type {
50
50
  const event = this._timeline.get(time);
51
- if (event) {
52
- return event.value;
53
- } else {
54
- return this._initialValue;
55
- }
51
+ return event?.value ?? this._initialValue;
56
52
  }
57
53
  }
@@ -58,11 +58,11 @@ export const singleIOProcess = /* javascript */ `
58
58
  const input = inputs[0];
59
59
  const output = outputs[0];
60
60
  // get the parameter values
61
- const channelCount = Math.max(input && input.length || 0, output.length);
61
+ const channelCount = Math.max(input?.length || 0, output.length);
62
62
  for (let sample = 0; sample < this.blockSize; sample++) {
63
63
  this.updateParams(parameters, sample);
64
64
  for (let channel = 0; channel < channelCount; channel++) {
65
- const inputSample = input && input.length ? input[channel][sample] : 0;
65
+ const inputSample = input?.length ? input[channel][sample] : 0;
66
66
  output[channel][sample] = this.generate(inputSample, channel, this.params);
67
67
  }
68
68
  }
@@ -97,10 +97,8 @@ export abstract class ToneAudioWorklet<
97
97
  dispose(): this {
98
98
  super.dispose();
99
99
  this._dummyGain.disconnect();
100
- if (this._worklet) {
101
- this._worklet.port.postMessage("dispose");
102
- this._worklet.disconnect();
103
- }
100
+ this._worklet?.port.postMessage("dispose");
101
+ this._worklet?.disconnect();
104
102
  return this;
105
103
  }
106
104
  }
@@ -10,7 +10,7 @@ import { TimeClass } from "./core/type/Time.js";
10
10
  import { TransportTimeClass } from "./core/type/TransportTime.js";
11
11
  import { omitFromObject } from "./core/util/Defaults.js";
12
12
  import { DrawInstance } from "./core/util/Draw.js";
13
- import { isDefined, isFunction } from "./core/util/TypeCheck.js";
13
+ import { isFunction } from "./core/util/TypeCheck.js";
14
14
 
15
15
  type ClassesWithoutSingletons = Omit<
16
16
  typeof Classes,
@@ -48,7 +48,7 @@ export function fromContext(context: Context): ToneObject {
48
48
  omitFromObject(Classes, ["Transport", "Destination", "Draw"])
49
49
  ).map((key) => {
50
50
  const cls = Classes[key];
51
- if (isDefined(cls) && isFunction(cls.getDefaults)) {
51
+ if (isFunction(cls?.getDefaults)) {
52
52
  classesWithContext[key] = class ToneFromContextNode extends cls {
53
53
  get defaultContext(): Context {
54
54
  return context;
@@ -264,7 +264,7 @@ export class Sampler extends Instrument<SamplerOptions> {
264
264
 
265
265
  // remove it when it's done
266
266
  source.onended = () => {
267
- if (this._activeSources && this._activeSources.has(midi)) {
267
+ if (this._activeSources?.has(midi)) {
268
268
  const sources = this._activeSources.get(
269
269
  midi
270
270
  ) as ToneBufferSource[];
@@ -31,6 +31,12 @@ describe("Noise", () => {
31
31
  noise.dispose();
32
32
  });
33
33
 
34
+ it("can be disposed while started", () => {
35
+ const noise = new Noise().toDestination();
36
+ noise.start();
37
+ noise.dispose();
38
+ });
39
+
34
40
  it("can set the playbackRate in the constructor", () => {
35
41
  const noise = new Noise({
36
42
  playbackRate: 2,
@@ -192,9 +192,7 @@ export class Noise extends Source<NoiseOptions> {
192
192
  */
193
193
  dispose(): this {
194
194
  super.dispose();
195
- if (this._source) {
196
- this._source.disconnect();
197
- }
195
+ this._source?.disconnect();
198
196
  return this;
199
197
  }
200
198
  }
@@ -330,6 +330,37 @@ describe("Source", () => {
330
330
  expect(output.getValueAtTime(0.5)).to.be.equal(0);
331
331
  });
332
332
 
333
+ it("stops the playing source when the transport is stopped and restarted at the same time", async () => {
334
+ const output = await Offline(({ transport }) => {
335
+ const source = new Player(rampBuffer).toDestination();
336
+ source.sync().start(0);
337
+ const clock = (
338
+ transport as unknown as {
339
+ _clock: {
340
+ context: typeof transport.context;
341
+ _boundLoop: () => void;
342
+ };
343
+ }
344
+ )._clock;
345
+ transport.start(0);
346
+ // hold the clock loop back so that stop/start land ahead of
347
+ // _lastUpdate, as they do in a realtime context where both
348
+ // default to now() = currentTime + lookAhead
349
+ clock.context.off("tick", clock._boundLoop);
350
+ return atTime(0.2, (time) => {
351
+ transport.stop(time);
352
+ transport.start(time, 0.5);
353
+ clock.context.on("tick", clock._boundLoop);
354
+ });
355
+ }, 0.5);
356
+ // before the restart the ramp plays from 0
357
+ expect(output.getValueAtTime(0.1)).to.be.closeTo(0.1, 0.01);
358
+ // after the restart only the source started at offset 0.5 plays;
359
+ // if the first source were orphaned the two ramps would sum
360
+ expect(output.getValueAtTime(0.3)).to.be.closeTo(0.6, 0.01);
361
+ expect(output.getValueAtTime(0.4)).to.be.closeTo(0.7, 0.01);
362
+ });
363
+
333
364
  it("gives the correct offset on time on start/stop events when started with an offset", async () => {
334
365
  const output = await Offline(({ transport }) => {
335
366
  const source = new Player(rampBuffer).toDestination();
@@ -201,7 +201,7 @@ export class UserMedia extends ToneAudioNode<UserMediaOptions> {
201
201
  * and "stopped" when the mic is closed.
202
202
  */
203
203
  get state() {
204
- return this._stream && this._stream.active ? "started" : "stopped";
204
+ return this._stream?.active ? "started" : "stopped";
205
205
  }
206
206
 
207
207
  /**
@@ -213,11 +213,7 @@ export class UserMedia extends ToneAudioNode<UserMediaOptions> {
213
213
  * device is not open.
214
214
  */
215
215
  get deviceId(): string | undefined {
216
- if (this._device) {
217
- return this._device.deviceId;
218
- } else {
219
- return undefined;
220
- }
216
+ return this._device?.deviceId;
221
217
  }
222
218
 
223
219
  /**
@@ -226,11 +222,7 @@ export class UserMedia extends ToneAudioNode<UserMediaOptions> {
226
222
  * Returns null when the device is not open.
227
223
  */
228
224
  get groupId(): string | undefined {
229
- if (this._device) {
230
- return this._device.groupId;
231
- } else {
232
- return undefined;
233
- }
225
+ return this._device?.groupId;
234
226
  }
235
227
 
236
228
  /**
@@ -239,11 +231,7 @@ export class UserMedia extends ToneAudioNode<UserMediaOptions> {
239
231
  * because of permissions.
240
232
  */
241
233
  get label(): string | undefined {
242
- if (this._device) {
243
- return this._device.label;
244
- } else {
245
- return undefined;
246
- }
234
+ return this._device?.label;
247
235
  }
248
236
 
249
237
  /**
@@ -515,7 +515,7 @@ export class Player extends Source<PlayerOptions> {
515
515
 
516
516
  // cancel the stop event since it's at a different time now
517
517
  const stopEvent = this._state.getNextState("stopped", now);
518
- if (stopEvent && stopEvent.implicitEnd) {
518
+ if (stopEvent?.implicitEnd) {
519
519
  this._state.cancel(stopEvent.time);
520
520
  this._activeSources.forEach((source) => source.cancelStop());
521
521
 
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.5.47";
1
+ export const version: string = "15.5.49";