mirror of
https://github.com/Tonejs/Tone.js
synced 2024-12-26 03:23:11 +00:00
feat: sub-tick scheduling
values are no longer rounded to the nearest tick, they can happen between tick values.
This commit is contained in:
parent
dc9de66401
commit
33e14d06eb
11 changed files with 63 additions and 32 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<TransportOptions> 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<TransportOptions> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ export class TimeClass<Type extends Seconds | Ticks = Seconds, Unit extends stri
|
|||
toTicks(): Ticks {
|
||||
const quarterTime = this._beatsToUnits(1);
|
||||
const quarters = this.valueOf() / quarterTime;
|
||||
return Math.round(quarters * this._getPPQ());
|
||||
return quarters * this._getPPQ();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -235,7 +235,7 @@ describe("Loop", () => {
|
|||
}
|
||||
}).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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -251,11 +251,11 @@ export class ToneEvent<ValueType = any> extends ToneWithContext<ToneEventOptions
|
|||
if (this._state.getValueAtTime(ticks) === "started") {
|
||||
this._state.setStateAtTime("stopped", ticks, { id: -1 });
|
||||
const previousEvent = this._state.getBefore(ticks);
|
||||
let reschedulTime = ticks;
|
||||
let rescheduleTime = ticks;
|
||||
if (previousEvent !== null) {
|
||||
reschedulTime = previousEvent.time;
|
||||
rescheduleTime = previousEvent.time;
|
||||
}
|
||||
this._rescheduleEvents(reschedulTime);
|
||||
this._rescheduleEvents(rescheduleTime);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ export class ToneEvent<ValueType = any> extends ToneWithContext<ToneEventOptions
|
|||
* Get the duration of the loop.
|
||||
*/
|
||||
protected _getLoopDuration(): Ticks {
|
||||
return Math.round((this._loopEnd - this._loopStart) / this._playbackRate);
|
||||
return (this._loopEnd - this._loopStart) / this._playbackRate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -322,7 +322,7 @@ export class ToneEvent<ValueType = any> extends ToneWithContext<ToneEventOptions
|
|||
}
|
||||
|
||||
/**
|
||||
* The playback rate of the note. Defaults to 1.
|
||||
* The playback rate of the event. Defaults to 1.
|
||||
* @example
|
||||
* const note = new Tone.ToneEvent();
|
||||
* note.loop = true;
|
||||
|
|
|
@ -368,7 +368,7 @@ describe("Source", () => {
|
|||
}, 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);
|
||||
});
|
||||
});
|
||||
|
|
2
package-lock.json
generated
2
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tone",
|
||||
"version": "14.7.0",
|
||||
"version": "14.8.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue