tone 15.5.42 → 15.5.44
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/Clock.test.ts +59 -0
- package/Tone/core/clock/Clock.ts +22 -1
- package/Tone/core/clock/Transport.test.ts +32 -0
- package/Tone/core/type/TransportTime.ts +16 -0
- package/Tone/event/Part.test.ts +54 -0
- package/Tone/event/ToneEvent.test.ts +25 -1
- package/Tone/event/ToneEvent.ts +9 -4
- package/Tone/version.ts +1 -1
- package/build/Tone.js +1 -1
- package/build/Tone.js.map +1 -1
- package/build/esm/core/clock/Clock.d.ts +8 -0
- package/build/esm/core/clock/Clock.js +22 -1
- package/build/esm/core/clock/Clock.js.map +1 -1
- package/build/esm/core/type/TransportTime.d.ts +4 -0
- package/build/esm/core/type/TransportTime.js +15 -0
- package/build/esm/core/type/TransportTime.js.map +1 -1
- package/build/esm/event/ToneEvent.js +6 -2
- package/build/esm/event/ToneEvent.js.map +1 -1
- package/build/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -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", () => {
|
package/Tone/core/clock/Clock.ts
CHANGED
|
@@ -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) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getContext } from "../Global.js";
|
|
2
2
|
import { Seconds, Ticks } from "../type/Units.js";
|
|
3
|
+
import { isDefined } from "../util/TypeCheck.js";
|
|
3
4
|
import { TimeClass } from "./Time.js";
|
|
4
5
|
import { TimeBaseUnit, TimeExpression, TimeValue } from "./TimeBase.js";
|
|
5
6
|
|
|
@@ -22,6 +23,21 @@ export class TransportTimeClass<
|
|
|
22
23
|
return this.context.transport.seconds as Type;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Return the time in ticks.
|
|
28
|
+
*/
|
|
29
|
+
toTicks(): Ticks {
|
|
30
|
+
if (isDefined(this._val)) {
|
|
31
|
+
return super.toTicks();
|
|
32
|
+
}
|
|
33
|
+
// read the transport's actual current tick instead of re-deriving it
|
|
34
|
+
// from elapsed seconds at the current bpm, which ignores tempo changes.
|
|
35
|
+
// round up to the next tick since the current one may already be
|
|
36
|
+
// in the middle of being processed by the clock.
|
|
37
|
+
const transport = this.context.transport;
|
|
38
|
+
return Math.ceil(transport.getTicksAtTime(transport.now()));
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
protected _getExpressions(): TimeExpression<Type> {
|
|
26
42
|
const expressions = super._getExpressions();
|
|
27
43
|
// Override the quantize ("@") handler so that it returns Transport time
|
package/Tone/event/Part.test.ts
CHANGED
|
@@ -928,5 +928,59 @@ describe("Part", () => {
|
|
|
928
928
|
}).to.throw(Error);
|
|
929
929
|
part.dispose();
|
|
930
930
|
});
|
|
931
|
+
|
|
932
|
+
it("`toTicks()` with no argument matches the transport's actual current tick after a bpm change (#693)", async () => {
|
|
933
|
+
let actualTicks = 0;
|
|
934
|
+
let noArgTicks = 0;
|
|
935
|
+
await Offline(({ transport }) => {
|
|
936
|
+
transport.bpm.value = 120;
|
|
937
|
+
transport.start(0);
|
|
938
|
+
return atTime(0.5, () => {
|
|
939
|
+
transport.bpm.value = 40;
|
|
940
|
+
actualTicks = transport.getTicksAtTime(transport.now());
|
|
941
|
+
noArgTicks = transport.toTicks();
|
|
942
|
+
});
|
|
943
|
+
}, 0.6);
|
|
944
|
+
expect(noArgTicks).to.be.closeTo(actualTicks, 1);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it("does not delay a part's note when it is (re)started with no time arg while the transport is running", async () => {
|
|
948
|
+
let callTime: number | undefined;
|
|
949
|
+
await Offline(({ transport }) => {
|
|
950
|
+
const part = new Part(
|
|
951
|
+
(time) => {
|
|
952
|
+
callTime = time;
|
|
953
|
+
},
|
|
954
|
+
[0]
|
|
955
|
+
);
|
|
956
|
+
transport.bpm.value = 120;
|
|
957
|
+
transport.start(0);
|
|
958
|
+
return atTime(0.5, () => {
|
|
959
|
+
part.start();
|
|
960
|
+
});
|
|
961
|
+
}, 1);
|
|
962
|
+
expect(callTime).to.be.closeTo(0.5, 0.02);
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it("does not leave an orphaned repeat after stopping a phase-shifted note before its first occurrence", async () => {
|
|
966
|
+
// a note's own repeat is phase-shifted from the part's start tick;
|
|
967
|
+
// stopping before a phase-shifted note's first occurrence used to miss
|
|
968
|
+
// that boundary, leaving an orphaned repeat that could later collide
|
|
969
|
+
// with a subsequent restart (#864)
|
|
970
|
+
const calls: number[] = [];
|
|
971
|
+
await Offline(({ transport }) => {
|
|
972
|
+
const part = new Part((time) => calls.push(time), [0, 0.5]);
|
|
973
|
+
part.loop = true;
|
|
974
|
+
part.loopEnd = 1;
|
|
975
|
+
transport.bpm.value = 120;
|
|
976
|
+
transport.start(0);
|
|
977
|
+
part.start(0);
|
|
978
|
+
// stop before the note at 0.5s into the loop ever gets to fire
|
|
979
|
+
part.stop(0.1);
|
|
980
|
+
// restart partway through the next cycle
|
|
981
|
+
part.start(200 / 384);
|
|
982
|
+
}, 2);
|
|
983
|
+
expect(calls.length).to.equal(4);
|
|
984
|
+
});
|
|
931
985
|
});
|
|
932
986
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect } from "chai";
|
|
2
2
|
|
|
3
3
|
import { BasicTests } from "../../test/helper/Basic.js";
|
|
4
|
-
import { Offline, whenBetween } from "../../test/helper/Offline.js";
|
|
4
|
+
import { atTime, Offline, whenBetween } from "../../test/helper/Offline.js";
|
|
5
5
|
import { Time } from "../core/type/Time.js";
|
|
6
6
|
import { noOp } from "../core/util/Interface.js";
|
|
7
7
|
import { ToneEvent } from "./ToneEvent.js";
|
|
@@ -362,6 +362,30 @@ describe("ToneEvent", () => {
|
|
|
362
362
|
}, 2);
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
+
it("keeps a still-playing segment running until its own scheduled stop, even when a later restart is already queued (#864)", async () => {
|
|
366
|
+
const callTimes: number[] = [];
|
|
367
|
+
await Offline(({ transport }) => {
|
|
368
|
+
const note = new ToneEvent({
|
|
369
|
+
loop: true,
|
|
370
|
+
loopEnd: 0.1,
|
|
371
|
+
callback(time): void {
|
|
372
|
+
callTimes.push(time);
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
note.start(0);
|
|
376
|
+
transport.start(0);
|
|
377
|
+
return atTime(0.3, () => {
|
|
378
|
+
// queue a stop/restart ahead of the still-playing segment
|
|
379
|
+
note.stop(transport.seconds + 0.5);
|
|
380
|
+
note.start(transport.seconds + 0.8);
|
|
381
|
+
});
|
|
382
|
+
}, 1.5);
|
|
383
|
+
// the live segment should keep firing up to its own stop (~0.8),
|
|
384
|
+
// not be cut short by the later start() call scheduled at the same time
|
|
385
|
+
const notesBetween = callTimes.filter((t) => t > 0.35 && t < 0.75);
|
|
386
|
+
expect(notesBetween.length).to.be.greaterThan(2);
|
|
387
|
+
});
|
|
388
|
+
|
|
365
389
|
it("loops the correct amount of times when the event is started in the transport's past", async () => {
|
|
366
390
|
let callCount = 0;
|
|
367
391
|
await Offline(({ transport }) => {
|
package/Tone/event/ToneEvent.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../core/type/Units.js";
|
|
17
17
|
import { defaultArg, optionsFromArguments } from "../core/util/Defaults.js";
|
|
18
18
|
import { noOp } from "../core/util/Interface.js";
|
|
19
|
+
import { clamp } from "../core/util/Math.js";
|
|
19
20
|
import {
|
|
20
21
|
BasicPlaybackState,
|
|
21
22
|
StateTimeline,
|
|
@@ -185,11 +186,15 @@ export class ToneEvent<ValueType = any> extends ToneWithContext<
|
|
|
185
186
|
if (isNumber(this._loop)) {
|
|
186
187
|
duration = this._loop * this._getLoopDuration();
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
+
// look up the next boundary in the same (unshifted) frame as
|
|
190
|
+
// event.time; startOffset can push startTick past it entirely,
|
|
191
|
+
// in which case this segment shouldn't schedule anything
|
|
192
|
+
const nextEvent = this._state.getAfter(event.time);
|
|
189
193
|
if (nextEvent !== null) {
|
|
190
|
-
duration =
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
duration = clamp(
|
|
195
|
+
nextEvent.time - startTick,
|
|
196
|
+
0,
|
|
197
|
+
duration
|
|
193
198
|
);
|
|
194
199
|
}
|
|
195
200
|
if (duration !== Infinity) {
|
package/Tone/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: string = "15.5.
|
|
1
|
+
export const version: string = "15.5.44";
|