diff --git a/Tone/core/clock/Transport.test.ts b/Tone/core/clock/Transport.test.ts index 1cf34542..d13ead18 100644 --- a/Tone/core/clock/Transport.test.ts +++ b/Tone/core/clock/Transport.test.ts @@ -636,7 +636,7 @@ describe("Transport", () => { invocations++; }, 0.1, 0); transport.start(); - }, 0.5).then(() => { + }, 0.51).then(() => { expect(invocations).to.equal(6); }); }); @@ -688,8 +688,8 @@ describe("Transport", () => { repeatCount++; }, 0.1, 0, 0.5); transport.start(); - }, 0.6).then(() => { - expect(repeatCount).to.equal(6); + }, 0.61).then(() => { + expect(repeatCount).to.equal(5); }); }); diff --git a/Tone/core/clock/Transport.ts b/Tone/core/clock/Transport.ts index b61ecfa8..47f3bff3 100644 --- a/Tone/core/clock/Transport.ts +++ b/Tone/core/clock/Transport.ts @@ -32,7 +32,7 @@ interface TransportOptions extends ToneWithContextOptions { ppq: number; } -type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart"; +type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart" | "ticks"; interface SyncedSignalEvent { signal: Signal; @@ -576,6 +576,7 @@ export class Transport extends ToneWithContext implements Emit // restart it with the new time this.emit("start", time, this._clock.getSecondsAtTime(time)); } else { + this.emit("ticks", now); this._clock.setTicksAtTime(t, now); } } @@ -587,7 +588,7 @@ export class Transport extends ToneWithContext implements Emit * @return The tick value at the given time. */ getTicksAtTime(time?: Time): Ticks { - return Math.round(this._clock.getTicksAtTime(time)); + return this._clock.getTicksAtTime(time); } /** diff --git a/Tone/core/clock/TransportEvent.ts b/Tone/core/clock/TransportEvent.ts index c0999c07..acd019e8 100644 --- a/Tone/core/clock/TransportEvent.ts +++ b/Tone/core/clock/TransportEvent.ts @@ -41,6 +41,12 @@ export class TransportEvent { */ private _once: boolean; + /** + * The remaining value between the passed in time, and Math.floor(time). + * This value is later added back when scheduling to get sub-tick precision. + */ + protected _remainderTime = 0; + /** * @param transport The transport object which the event belongs to */ @@ -51,7 +57,8 @@ export class TransportEvent { this.transport = transport; this.callback = options.callback; this._once = options.once; - this.time = options.time; + this.time = Math.floor(options.time); + this._remainderTime = options.time - this.time; } static getDefaults(): TransportEventOptions { @@ -67,13 +74,21 @@ export class TransportEvent { */ private static _eventId = 0; + /** + * Get the time and remainder time. + */ + protected get floatTime(): number { + return this.time + this._remainderTime; + } + /** * Invoke the event callback. * @param time The AudioContext time in seconds of the event */ invoke(time: Seconds): void { if (this.callback) { - this.callback(time); + const tickDuration = this.transport.bpm.getDurationOfTicks(1, time); + this.callback(time + this._remainderTime * tickDuration); if (this._once) { this.transport.clear(this.id); } diff --git a/Tone/core/clock/TransportRepeatEvent.ts b/Tone/core/clock/TransportRepeatEvent.ts index d549c9ca..b680d52c 100644 --- a/Tone/core/clock/TransportRepeatEvent.ts +++ b/Tone/core/clock/TransportRepeatEvent.ts @@ -2,6 +2,7 @@ import { BaseContext } from "../context/BaseContext"; import { TicksClass } from "../type/Ticks"; import { Seconds, Ticks, Time } from "../type/Units"; import { TransportEvent, TransportEventOptions } from "./TransportEvent"; +import { GT, LT } from "../util/Math"; type Transport = import("../clock/Transport").Transport; @@ -60,11 +61,12 @@ export class TransportRepeatEvent extends TransportEvent { const options = Object.assign(TransportRepeatEvent.getDefaults(), opts); - this.duration = new TicksClass(transport.context, options.duration).valueOf(); - this._interval = new TicksClass(transport.context, options.interval).valueOf(); + this.duration = options.duration; + this._interval = options.interval; this._nextTick = options.time; this.transport.on("start", this._boundRestart); this.transport.on("loopStart", this._boundRestart); + this.transport.on("ticks", this._boundRestart); this.context = this.transport.context; this._restart(); } @@ -89,13 +91,25 @@ export class TransportRepeatEvent extends TransportEvent { super.invoke(time); } + /** + * Create an event on the transport on the nextTick + */ + private _createEvent(): number { + if (LT(this._nextTick, this.floatTime + this.duration)) { + return this.transport.scheduleOnce(this.invoke.bind(this), + new TicksClass(this.context, this._nextTick).toSeconds()); + } + return -1; + } + /** * Push more events onto the timeline to keep up with the position of the timeline */ private _createEvents(time: Seconds): void { // schedule the next event - const ticks = this.transport.getTicksAtTime(time); - if (ticks >= this.time && ticks >= this._nextTick && this._nextTick + this._interval < this.time + this.duration) { + // const ticks = this.transport.getTicksAtTime(time); + // if the next tick is within the bounds set by "duration" + if (LT(this._nextTick + this._interval, this.floatTime + this.duration)) { this._nextTick += this._interval; this._currentId = this._nextId; this._nextId = this.transport.scheduleOnce(this.invoke.bind(this), @@ -104,21 +118,21 @@ export class TransportRepeatEvent extends TransportEvent { } /** - * Push more events onto the timeline to keep up with the position of the timeline + * Re-compute the events when the transport time has changed from a start/ticks/loopStart event */ private _restart(time?: Time): void { this.transport.clear(this._currentId); this.transport.clear(this._nextId); - this._nextTick = this.time; + // start at the first event + this._nextTick = this.floatTime; const ticks = this.transport.getTicksAtTime(time); - if (ticks > this.time) { - this._nextTick = this.time + Math.ceil((ticks - this.time) / this._interval) * this._interval; + if (GT(ticks, this.time)) { + // the event is not being scheduled from the beginning and should be offset + this._nextTick = this.floatTime + Math.ceil((ticks - this.floatTime) / this._interval) * this._interval; } - this._currentId = this.transport.scheduleOnce(this.invoke.bind(this), - new TicksClass(this.context, this._nextTick).toSeconds()); + this._currentId = this._createEvent(); this._nextTick += this._interval; - this._nextId = this.transport.scheduleOnce(this.invoke.bind(this), - new TicksClass(this.context, this._nextTick).toSeconds()); + this._nextId = this._createEvent(); } /** @@ -130,6 +144,7 @@ export class TransportRepeatEvent extends TransportEvent { this.transport.clear(this._nextId); this.transport.off("start", this._boundRestart); this.transport.off("loopStart", this._boundRestart); + this.transport.off("ticks", this._boundRestart); return this; } } diff --git a/Tone/core/type/Time.ts b/Tone/core/type/Time.ts index ae22633a..1af83730 100644 --- a/Tone/core/type/Time.ts +++ b/Tone/core/type/Time.ts @@ -113,7 +113,7 @@ export class TimeClass { } }).start(0); transport.start(); - }, 0.8).then(() => { + }, 0.81).then(() => { expect(callCount).to.equal(9); }); }); @@ -326,7 +326,7 @@ describe("Loop", () => { loop.playbackRate = 1.5; expect(loop.playbackRate).to.equal(1.5); transport.start(); - }, 0.8).then(() => { + }, 0.81).then(() => { expect(callCount).to.equal(13); }); }); diff --git a/Tone/event/ToneEvent.test.ts b/Tone/event/ToneEvent.test.ts index 607d0d70..573f9fd6 100644 --- a/Tone/event/ToneEvent.test.ts +++ b/Tone/event/ToneEvent.test.ts @@ -355,11 +355,11 @@ describe("ToneEvent", () => { it("can be started and stopped multiple times", () => { return Offline(({ transport }) => { - const eventTimes = [0.3, 0.4, 0.9, 1.0, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9]; + const eventTimes = [0.3, 0.39, 0.9, 0.99, 1.3, 1.39, 1.48, 1.57, 1.66, 1.75, 1.84]; let eventTimeIndex = 0; new ToneEvent({ loop: true, - loopEnd: 0.1, + loopEnd: 0.09, callback(time): void { expect(eventTimes.length).to.be.gt(eventTimeIndex); expect(eventTimes[eventTimeIndex]).to.be.closeTo(time, 0.05); diff --git a/Tone/event/ToneEvent.ts b/Tone/event/ToneEvent.ts index f78ac594..eba69053 100644 --- a/Tone/event/ToneEvent.ts +++ b/Tone/event/ToneEvent.ts @@ -251,11 +251,11 @@ export class ToneEvent extends ToneWithContext extends ToneWithContext extends ToneWithContext { }, 0.7).then(output => { expect(output.getValueAtTime(0.01)).to.be.closeTo(0.2, 0.01); expect(output.getValueAtTime(0.1)).to.be.closeTo(0.3, 0.01); - expect(output.getValueAtTime(0.2)).to.be.closeTo(0.4, 0.01); + expect(output.getValueAtTime(0.199)).to.be.closeTo(0.4, 0.01); expect(output.getValueAtTime(0.31)).to.be.equal(0); }); }); diff --git a/package-lock.json b/package-lock.json index 9df2639c..1140a515 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tone", - "version": "14.7.0", + "version": "14.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4a0a63d0..bee9374d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tone", - "version": "14.7.0", + "version": "14.8.0", "description": "A Web Audio framework for making interactive music in the browser.", "main": "build/Tone.js", "module": "build/esm/index.js",