tone 15.5.18 → 15.5.20
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.
- package/Tone/core/clock/TickSource.test.ts +59 -0
- package/Tone/core/clock/TickSource.ts +17 -7
- package/Tone/core/clock/Transport.test.ts +14 -0
- package/Tone/core/context/Context.test.ts +10 -0
- package/Tone/core/context/Context.ts +33 -8
- package/Tone/core/util/Timeline.test.ts +4 -0
- package/Tone/core/util/Timeline.ts +30 -2
- package/Tone/version.ts +1 -1
- package/build/Tone.js +1 -1
- package/build/Tone.js.map +1 -1
- package/build/esm/core/clock/TickSource.js +14 -5
- package/build/esm/core/clock/TickSource.js.map +1 -1
- package/build/esm/core/context/Context.d.ts +12 -0
- package/build/esm/core/context/Context.js +30 -8
- package/build/esm/core/context/Context.js.map +1 -1
- package/build/esm/core/util/Timeline.d.ts +4 -0
- package/build/esm/core/util/Timeline.js +31 -2
- package/build/esm/core/util/Timeline.js.map +1 -1
- package/build/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -544,6 +544,65 @@ describe("TickSource", () => {
|
|
|
544
544
|
});
|
|
545
545
|
source.dispose();
|
|
546
546
|
});
|
|
547
|
+
|
|
548
|
+
it("does not fire duplicate ticks across consecutive windows at a tick boundary", () => {
|
|
549
|
+
// Reproduces the double-trigger bug from issues #1080/#1098/#1175.
|
|
550
|
+
// At 400 ticks/sec (equivalent to BPM=125, PPQ=192), tick 7680 falls
|
|
551
|
+
// at exactly t=19.2s.
|
|
552
|
+
const source = new TickSource(400);
|
|
553
|
+
source.start(0);
|
|
554
|
+
|
|
555
|
+
const firedTicks: number[] = [];
|
|
556
|
+
// Window 1 ends just above the tick-7680 boundary
|
|
557
|
+
source.forEachTickBetween(
|
|
558
|
+
19.19709750566994,
|
|
559
|
+
19.20000000000101,
|
|
560
|
+
(_, tick) => firedTicks.push(tick)
|
|
561
|
+
);
|
|
562
|
+
// Window 2 starts at exactly that same boundary
|
|
563
|
+
source.forEachTickBetween(
|
|
564
|
+
19.20000000000101,
|
|
565
|
+
19.202902494332076,
|
|
566
|
+
(_, tick) => firedTicks.push(tick)
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
const occurrences = firedTicks.filter((t) => t === 7680).length;
|
|
570
|
+
expect(occurrences).to.equal(
|
|
571
|
+
1,
|
|
572
|
+
`tick 7680 should fire exactly once, but fired ${occurrences} times`
|
|
573
|
+
);
|
|
574
|
+
source.dispose();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("fires every tick exactly once across many consecutive windows", () => {
|
|
578
|
+
// At 2 Hz, ticks fall at 0, 0.5, 1.0, 1.5, 2.0.
|
|
579
|
+
// Window boundaries are placed just above each tick time (tick + eps)
|
|
580
|
+
// so every tick lands right at the end of one window and the start of the next.
|
|
581
|
+
const source = new TickSource(2);
|
|
582
|
+
source.start(0);
|
|
583
|
+
|
|
584
|
+
const tickCount = new Map<number, number>();
|
|
585
|
+
const record = (_: number, tick: number) => {
|
|
586
|
+
tickCount.set(tick, (tickCount.get(tick) ?? 0) + 1);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Non-overlapping consecutive windows whose boundaries sit just above
|
|
590
|
+
// the tick times (0, 0.5, 1.0, 1.5, 2.0).
|
|
591
|
+
const eps = 1e-10;
|
|
592
|
+
source.forEachTickBetween(0, 0.5 + eps, record); // tick 0 (at 0.0) and tick 1 (at 0.5)
|
|
593
|
+
source.forEachTickBetween(0.5 + eps, 1.0 + eps, record); // tick 2 (at 1.0)
|
|
594
|
+
source.forEachTickBetween(1.0 + eps, 1.5 + eps, record); // tick 3 (at 1.5)
|
|
595
|
+
source.forEachTickBetween(1.5 + eps, 2.0 + eps, record); // tick 4 (at 2.0)
|
|
596
|
+
source.forEachTickBetween(2.0 + eps, 2.5, record); // tick 5 (at 2.5, not included)
|
|
597
|
+
|
|
598
|
+
for (const [tick, count] of tickCount) {
|
|
599
|
+
expect(count).to.equal(
|
|
600
|
+
1,
|
|
601
|
+
`tick ${tick} fired ${count} times instead of once`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
source.dispose();
|
|
605
|
+
});
|
|
547
606
|
});
|
|
548
607
|
|
|
549
608
|
context("Seconds", () => {
|
|
@@ -415,18 +415,28 @@ export class TickSource<
|
|
|
415
415
|
|
|
416
416
|
if (lastStateEvent && lastStateEvent.state === "started") {
|
|
417
417
|
const maxStartTime = Math.max(lastStateEvent.time, startTime);
|
|
418
|
-
//
|
|
418
|
+
// Figure out how far past the last whole-tick boundary maxStartTime
|
|
419
|
+
// sits, so we can compute the time of the next tick at or after it.
|
|
419
420
|
const startTicks = this.frequency.getTicksAtTime(maxStartTime);
|
|
420
421
|
const ticksAtStart = this.frequency.getTicksAtTime(
|
|
421
422
|
lastStateEvent.time
|
|
422
423
|
);
|
|
423
424
|
const diff = startTicks - ticksAtStart;
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
startTicks
|
|
429
|
-
|
|
425
|
+
const offset = Math.ceil(diff) - diff;
|
|
426
|
+
// Guard against floating-point issues: when startTicks is just barely
|
|
427
|
+
// above an integer tick boundary (offset ≈ 1), snap back to that integer
|
|
428
|
+
const firstTick = EQ(offset, 1)
|
|
429
|
+
? Math.floor(startTicks)
|
|
430
|
+
: startTicks + offset;
|
|
431
|
+
let nextTickTime = this.frequency.getTimeOfTick(firstTick);
|
|
432
|
+
// Advance past any ticks that land before the start of this window
|
|
433
|
+
// to avoid any tick that was already processed.
|
|
434
|
+
if (nextTickTime < maxStartTime) {
|
|
435
|
+
nextTickTime += this.frequency.getDurationOfTicks(
|
|
436
|
+
1,
|
|
437
|
+
nextTickTime
|
|
438
|
+
);
|
|
439
|
+
}
|
|
430
440
|
while (nextTickTime < endTime) {
|
|
431
441
|
try {
|
|
432
442
|
callback(
|
|
@@ -75,6 +75,20 @@ describe("Transport", () => {
|
|
|
75
75
|
expect(invocations).to.equal(5);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
it("does not double-fire events when looping many times", async () => {
|
|
79
|
+
let invocations = 0;
|
|
80
|
+
await Offline((context) => {
|
|
81
|
+
const transport = new TransportInstance({ context });
|
|
82
|
+
transport.schedule(() => {
|
|
83
|
+
invocations++;
|
|
84
|
+
}, 0);
|
|
85
|
+
transport.setLoopPoints(0, 1).start(0);
|
|
86
|
+
transport.loop = true;
|
|
87
|
+
}, 120.1);
|
|
88
|
+
// Should fire exactly 121 times: at t=0,1,2,...,120
|
|
89
|
+
expect(invocations).to.equal(121);
|
|
90
|
+
});
|
|
91
|
+
|
|
78
92
|
it("jumps to the loopStart after the loopEnd point", async () => {
|
|
79
93
|
let looped = false;
|
|
80
94
|
await Offline((context) => {
|
|
@@ -221,6 +221,16 @@ describe("Context", () => {
|
|
|
221
221
|
}, 0.05);
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
+
it("can efficiently add and remove 10k timeouts", () => {
|
|
225
|
+
const ids: number[] = [];
|
|
226
|
+
for (let i = 0; i < 10_000; i++) {
|
|
227
|
+
ids.push(ctx.setTimeout(() => {}, i * 0.001));
|
|
228
|
+
}
|
|
229
|
+
for (const id of ids) {
|
|
230
|
+
ctx.clearTimeout(id);
|
|
231
|
+
}
|
|
232
|
+
}).timeout(100);
|
|
233
|
+
|
|
224
234
|
it("is robust against altering the timeline within the callback fn", (done) => {
|
|
225
235
|
let invokeCount = 0;
|
|
226
236
|
function checkDone(id: number) {
|
|
@@ -64,6 +64,11 @@ export class Context extends BaseContext {
|
|
|
64
64
|
*/
|
|
65
65
|
private _timeouts: Timeline<ContextTimeoutEvent> = new Timeline();
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A Map from timeout ID to event for O(1) lookup in clearTimeout.
|
|
69
|
+
*/
|
|
70
|
+
private _timeoutMap = new Map<number, ContextTimeoutEvent>();
|
|
71
|
+
|
|
67
72
|
/**
|
|
68
73
|
* The timeout id counter
|
|
69
74
|
*/
|
|
@@ -554,6 +559,7 @@ export class Context extends BaseContext {
|
|
|
554
559
|
super.dispose();
|
|
555
560
|
this._ticker.dispose();
|
|
556
561
|
this._timeouts.dispose();
|
|
562
|
+
this._timeoutMap.clear();
|
|
557
563
|
Object.keys(this._constants).map((val) =>
|
|
558
564
|
this._constants[val].disconnect()
|
|
559
565
|
);
|
|
@@ -565,6 +571,26 @@ export class Context extends BaseContext {
|
|
|
565
571
|
// TIMEOUTS
|
|
566
572
|
//---------------------------
|
|
567
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Add a timeout event to the timeline and map.
|
|
576
|
+
*/
|
|
577
|
+
private _addTimeoutEvent(event: ContextTimeoutEvent): void {
|
|
578
|
+
this._timeouts.add(event);
|
|
579
|
+
this._timeoutMap.set(event.id, event);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Remove a timeout event from the timeline and map.
|
|
584
|
+
*/
|
|
585
|
+
private _removeTimeoutEvent(event: ContextTimeoutEvent): void {
|
|
586
|
+
this._timeouts.remove(event);
|
|
587
|
+
// Skips the map deletion in setInterval case where the same id
|
|
588
|
+
// is reused for the next event.
|
|
589
|
+
if (this._timeoutMap.get(event.id) === event) {
|
|
590
|
+
this._timeoutMap.delete(event.id);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
568
594
|
/**
|
|
569
595
|
* The private loop which keeps track of the context scheduled timeouts
|
|
570
596
|
* Is invoked from the clock source
|
|
@@ -575,7 +601,7 @@ export class Context extends BaseContext {
|
|
|
575
601
|
try {
|
|
576
602
|
event.callback();
|
|
577
603
|
} finally {
|
|
578
|
-
this.
|
|
604
|
+
this._removeTimeoutEvent(event);
|
|
579
605
|
}
|
|
580
606
|
});
|
|
581
607
|
}
|
|
@@ -592,7 +618,7 @@ export class Context extends BaseContext {
|
|
|
592
618
|
setTimeout(fn: (...args: any[]) => void, timeout: Seconds): number {
|
|
593
619
|
this._timeoutIds++;
|
|
594
620
|
const now = this.now();
|
|
595
|
-
this.
|
|
621
|
+
this._addTimeoutEvent({
|
|
596
622
|
callback: fn,
|
|
597
623
|
id: this._timeoutIds,
|
|
598
624
|
time: now + timeout,
|
|
@@ -605,11 +631,10 @@ export class Context extends BaseContext {
|
|
|
605
631
|
* @param id The ID returned from {@link setTimeout}.
|
|
606
632
|
*/
|
|
607
633
|
clearTimeout(id: number): this {
|
|
608
|
-
this.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
});
|
|
634
|
+
const event = this._timeoutMap.get(id);
|
|
635
|
+
if (event) {
|
|
636
|
+
this._removeTimeoutEvent(event);
|
|
637
|
+
}
|
|
613
638
|
return this;
|
|
614
639
|
}
|
|
615
640
|
|
|
@@ -631,7 +656,7 @@ export class Context extends BaseContext {
|
|
|
631
656
|
const id = ++this._timeoutIds;
|
|
632
657
|
const intervalFn = () => {
|
|
633
658
|
const now = this.now();
|
|
634
|
-
this.
|
|
659
|
+
this._addTimeoutEvent({
|
|
635
660
|
callback: () => {
|
|
636
661
|
// invoke the callback
|
|
637
662
|
fn();
|
|
@@ -93,6 +93,10 @@ describe("Timeline", () => {
|
|
|
93
93
|
sched.remove({ time: 4 });
|
|
94
94
|
});
|
|
95
95
|
expect(sched.length).to.equal(1);
|
|
96
|
+
sched.add({ time: 1 });
|
|
97
|
+
sched.add({ time: 3 });
|
|
98
|
+
sched.remove({ time: 2 });
|
|
99
|
+
expect(sched.length).to.equal(3);
|
|
96
100
|
sched.dispose();
|
|
97
101
|
});
|
|
98
102
|
|
|
@@ -115,7 +115,7 @@ export class Timeline<GenericEvent extends TimelineEvent> extends Tone {
|
|
|
115
115
|
* @returns {Timeline} this
|
|
116
116
|
*/
|
|
117
117
|
remove(event: GenericEvent): this {
|
|
118
|
-
const index = this.
|
|
118
|
+
const index = this._indexOf(event);
|
|
119
119
|
if (index !== -1) {
|
|
120
120
|
this._timeline.splice(index, 1);
|
|
121
121
|
}
|
|
@@ -240,7 +240,7 @@ export class Timeline<GenericEvent extends TimelineEvent> extends Tone {
|
|
|
240
240
|
* @return The event right before the given event
|
|
241
241
|
*/
|
|
242
242
|
previousEvent(event: GenericEvent): GenericEvent | null {
|
|
243
|
-
const index = this.
|
|
243
|
+
const index = this._indexOf(event);
|
|
244
244
|
if (index > 0) {
|
|
245
245
|
return this._timeline[index - 1];
|
|
246
246
|
} else {
|
|
@@ -248,6 +248,34 @@ export class Timeline<GenericEvent extends TimelineEvent> extends Tone {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Return the index of the given event in the timeline. -1 if it is not found.
|
|
253
|
+
*/
|
|
254
|
+
private _indexOf(event: GenericEvent): number {
|
|
255
|
+
// Use _search since internally implemented with binary search.
|
|
256
|
+
const index = this._search(event.time);
|
|
257
|
+
if (index === -1) {
|
|
258
|
+
return -1;
|
|
259
|
+
}
|
|
260
|
+
// There may be multiple events with the same time, so find the exact event
|
|
261
|
+
// Search forward and backward for events with the same time
|
|
262
|
+
for (let i = index; i < this._timeline.length; i++) {
|
|
263
|
+
if (this._timeline[i] === event) {
|
|
264
|
+
return i;
|
|
265
|
+
} else if (this._timeline[i].time !== event.time) {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
270
|
+
if (this._timeline[j] === event) {
|
|
271
|
+
return j;
|
|
272
|
+
} else if (this._timeline[j].time !== event.time) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return -1;
|
|
277
|
+
}
|
|
278
|
+
|
|
251
279
|
/**
|
|
252
280
|
* Does a binary search on the timeline array and returns the
|
|
253
281
|
* nearest event index whose time is after or equal to the given time.
|
package/Tone/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: string = "15.5.
|
|
1
|
+
export const version: string = "15.5.20";
|