tone 15.5.41 → 15.5.43

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.
@@ -354,6 +354,65 @@ describe("Clock", () => {
354
354
  clock.start(0, 4);
355
355
  });
356
356
  });
357
+
358
+ it("does not replay stale pre-restart ticks when the loop update has fallen behind a restart (#1419)", async () => {
359
+ const observedTicks: Array<number | undefined> = [];
360
+ await Offline(() => {
361
+ const clock = new Clock((_t, ticks) => {
362
+ observedTicks.push(ticks);
363
+ }, 200);
364
+ const boundLoop = (
365
+ clock as unknown as { _boundLoop: () => void }
366
+ )._boundLoop;
367
+ clock.start(0);
368
+ // Detach the clock's own loop from the context's "tick" event
369
+ // so _lastUpdate genuinely never catches up, simulating the
370
+ // tick loop's cadence (default updateInterval=0.05s) falling
371
+ // behind a rapid stop/start cycle, as described in #1419.
372
+ clock.context.off("tick", boundLoop);
373
+ return atTime(0.02, (time) => {
374
+ clock.stop(time);
375
+ clock.start(time, 40);
376
+ // Reattach the loop: without a fix, it would replay ticks
377
+ // from [_lastUpdate, time) using the pre-restart TickSource
378
+ // state instead of the new offset.
379
+ clock.context.on("tick", boundLoop);
380
+ });
381
+ }, 0.03);
382
+ expect(observedTicks).to.deep.equal([0, 1, 2, 3, 4, 40, 41]);
383
+ });
384
+
385
+ it("does not drop the pending stop event or pending ticks when stop and the following start happen at different times within the stale window (#1419)", async () => {
386
+ const observedTicks: Array<number | undefined> = [];
387
+ const stopEvents: number[] = [];
388
+ await Offline(() => {
389
+ const clock = new Clock((_t, ticks) => {
390
+ observedTicks.push(ticks);
391
+ }, 200);
392
+ const boundLoop = (
393
+ clock as unknown as { _boundLoop: () => void }
394
+ )._boundLoop;
395
+ clock.on("stop", (time) => {
396
+ stopEvents.push(time);
397
+ });
398
+ clock.start(0);
399
+ // Same staleness simulation as above, but this time stop() and
400
+ // the following start() are scheduled at two distinct times
401
+ // within the stale window, rather than the same instant.
402
+ clock.context.off("tick", boundLoop);
403
+ return atTime(0.025, () => {
404
+ clock.stop(0.015);
405
+ clock.start(0.02, 40);
406
+ clock.context.on("tick", boundLoop);
407
+ });
408
+ }, 0.03);
409
+ // the "stop" event scheduled at 0.015 must still fire
410
+ expect(stopEvents).to.deep.equal([0.015]);
411
+ // ticks 0, 1, 2 (at 200Hz: 0, 0.005, 0.01) are still pending
412
+ // in [_lastUpdate, 0.015) and must still fire, followed by
413
+ // the post-restart ticks from offset 40
414
+ expect(observedTicks).to.deep.equal([0, 1, 2, 40, 41]);
415
+ });
357
416
  });
358
417
 
359
418
  context("Events", () => {
@@ -133,6 +133,17 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
133
133
  const computedTime = this.toSeconds(time);
134
134
  this.log("start", computedTime);
135
135
  if (this._state.getValueAtTime(computedTime) !== "started") {
136
+ if (this._lastUpdate < computedTime && computedTime <= this.now()) {
137
+ // _lastUpdate has not yet caught up to this restart (e.g.
138
+ // rapid stop/start cycles outrunning the tick loop's
139
+ // cadence). Flush the state-transition events (e.g. a
140
+ // pending "stop") and tick callbacks still waiting in
141
+ // [_lastUpdate, computedTime) using the pre-restart
142
+ // TickSource state before it's reset below, so they aren't
143
+ // silently dropped, and so the next _loop() pass doesn't
144
+ // replay them using the post-restart state.
145
+ this._processRange(this._lastUpdate, computedTime);
146
+ }
136
147
  this._state.setStateAtTime("started", computedTime);
137
148
  this._tickSource.start(computedTime, offset);
138
149
  if (computedTime < this._lastUpdate) {
@@ -267,9 +278,19 @@ export class Clock<TypeName extends "bpm" | "hertz" = "hertz">
267
278
  private _loop(): void {
268
279
  const startTime = this._lastUpdate;
269
280
  const endTime = this.now();
270
- this._lastUpdate = endTime;
271
281
  this.log("loop", startTime, endTime);
282
+ this._processRange(startTime, endTime);
283
+ }
272
284
 
285
+ /**
286
+ * Invoke the state-transition events (start/stop/pause) and tick
287
+ * callbacks scheduled between startTime and endTime, and advance
288
+ * _lastUpdate to endTime.
289
+ * @param startTime The beginning of the range to process.
290
+ * @param endTime The end of the range to process.
291
+ */
292
+ private _processRange(startTime: number, endTime: number): void {
293
+ this._lastUpdate = endTime;
273
294
  if (startTime !== endTime) {
274
295
  // the state change events
275
296
  this._state.forEachBetween(startTime, endTime, (e) => {
@@ -1001,6 +1001,38 @@ describe("Transport", () => {
1001
1001
  expect(invocations).to.equal(3);
1002
1002
  });
1003
1003
 
1004
+ it("does not drop the transport's stop event when stop/start outrun the clock's tick loop (#1419)", async () => {
1005
+ const stopEvents: number[] = [];
1006
+ await Offline((context) => {
1007
+ const transport = new TransportInstance({ context });
1008
+ transport.on("stop", (time) => {
1009
+ stopEvents.push(time);
1010
+ });
1011
+ const clock = (
1012
+ transport as unknown as {
1013
+ _clock: {
1014
+ context: typeof context;
1015
+ _boundLoop: () => void;
1016
+ };
1017
+ }
1018
+ )._clock;
1019
+ transport.start(0);
1020
+ // Detach the clock's own loop from the context's "tick"
1021
+ // event so its _lastUpdate genuinely falls behind,
1022
+ // simulating the tick loop's cadence (default
1023
+ // updateInterval=0.05s) outrunning a rapid
1024
+ // Transport.stop()/Transport.start() cycle, as described
1025
+ // in #1419.
1026
+ clock.context.off("tick", clock._boundLoop);
1027
+ return atTime(0.02, () => {
1028
+ transport.stop(0.015);
1029
+ transport.start(0.018);
1030
+ clock.context.on("tick", clock._boundLoop);
1031
+ });
1032
+ }, 0.03);
1033
+ expect(stopEvents).to.deep.equal([0.015]);
1034
+ });
1035
+
1004
1036
  it("invokes start event with correct offset", async () => {
1005
1037
  let wasCalled = false;
1006
1038
  await Offline((context) => {
package/Tone/version.ts CHANGED
@@ -1 +1 @@
1
- export const version: string = "15.5.41";
1
+ export const version: string = "15.5.43";