tone 15.5.48 → 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.
@@ -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", () => {
@@ -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);
@@ -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
 
@@ -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();
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.5.48";
1
+ export const version: string = "15.5.49";