mirror of
https://github.com/Tonejs/Tone.js
synced 2024-12-26 19:43:12 +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++;
|
invocations++;
|
||||||
}, 0.1, 0);
|
}, 0.1, 0);
|
||||||
transport.start();
|
transport.start();
|
||||||
}, 0.5).then(() => {
|
}, 0.51).then(() => {
|
||||||
expect(invocations).to.equal(6);
|
expect(invocations).to.equal(6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -688,8 +688,8 @@ describe("Transport", () => {
|
||||||
repeatCount++;
|
repeatCount++;
|
||||||
}, 0.1, 0, 0.5);
|
}, 0.1, 0, 0.5);
|
||||||
transport.start();
|
transport.start();
|
||||||
}, 0.6).then(() => {
|
}, 0.61).then(() => {
|
||||||
expect(repeatCount).to.equal(6);
|
expect(repeatCount).to.equal(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ interface TransportOptions extends ToneWithContextOptions {
|
||||||
ppq: number;
|
ppq: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart";
|
type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart" | "ticks";
|
||||||
|
|
||||||
interface SyncedSignalEvent {
|
interface SyncedSignalEvent {
|
||||||
signal: Signal;
|
signal: Signal;
|
||||||
|
@ -576,6 +576,7 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
|
||||||
// restart it with the new time
|
// restart it with the new time
|
||||||
this.emit("start", time, this._clock.getSecondsAtTime(time));
|
this.emit("start", time, this._clock.getSecondsAtTime(time));
|
||||||
} else {
|
} else {
|
||||||
|
this.emit("ticks", now);
|
||||||
this._clock.setTicksAtTime(t, 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.
|
* @return The tick value at the given time.
|
||||||
*/
|
*/
|
||||||
getTicksAtTime(time?: Time): Ticks {
|
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;
|
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
|
* @param transport The transport object which the event belongs to
|
||||||
*/
|
*/
|
||||||
|
@ -51,7 +57,8 @@ export class TransportEvent {
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
this.callback = options.callback;
|
this.callback = options.callback;
|
||||||
this._once = options.once;
|
this._once = options.once;
|
||||||
this.time = options.time;
|
this.time = Math.floor(options.time);
|
||||||
|
this._remainderTime = options.time - this.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDefaults(): TransportEventOptions {
|
static getDefaults(): TransportEventOptions {
|
||||||
|
@ -67,13 +74,21 @@ export class TransportEvent {
|
||||||
*/
|
*/
|
||||||
private static _eventId = 0;
|
private static _eventId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time and remainder time.
|
||||||
|
*/
|
||||||
|
protected get floatTime(): number {
|
||||||
|
return this.time + this._remainderTime;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the event callback.
|
* Invoke the event callback.
|
||||||
* @param time The AudioContext time in seconds of the event
|
* @param time The AudioContext time in seconds of the event
|
||||||
*/
|
*/
|
||||||
invoke(time: Seconds): void {
|
invoke(time: Seconds): void {
|
||||||
if (this.callback) {
|
if (this.callback) {
|
||||||
this.callback(time);
|
const tickDuration = this.transport.bpm.getDurationOfTicks(1, time);
|
||||||
|
this.callback(time + this._remainderTime * tickDuration);
|
||||||
if (this._once) {
|
if (this._once) {
|
||||||
this.transport.clear(this.id);
|
this.transport.clear(this.id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { BaseContext } from "../context/BaseContext";
|
||||||
import { TicksClass } from "../type/Ticks";
|
import { TicksClass } from "../type/Ticks";
|
||||||
import { Seconds, Ticks, Time } from "../type/Units";
|
import { Seconds, Ticks, Time } from "../type/Units";
|
||||||
import { TransportEvent, TransportEventOptions } from "./TransportEvent";
|
import { TransportEvent, TransportEventOptions } from "./TransportEvent";
|
||||||
|
import { GT, LT } from "../util/Math";
|
||||||
|
|
||||||
type Transport = import("../clock/Transport").Transport;
|
type Transport = import("../clock/Transport").Transport;
|
||||||
|
|
||||||
|
@ -60,11 +61,12 @@ export class TransportRepeatEvent extends TransportEvent {
|
||||||
|
|
||||||
const options = Object.assign(TransportRepeatEvent.getDefaults(), opts);
|
const options = Object.assign(TransportRepeatEvent.getDefaults(), opts);
|
||||||
|
|
||||||
this.duration = new TicksClass(transport.context, options.duration).valueOf();
|
this.duration = options.duration;
|
||||||
this._interval = new TicksClass(transport.context, options.interval).valueOf();
|
this._interval = options.interval;
|
||||||
this._nextTick = options.time;
|
this._nextTick = options.time;
|
||||||
this.transport.on("start", this._boundRestart);
|
this.transport.on("start", this._boundRestart);
|
||||||
this.transport.on("loopStart", this._boundRestart);
|
this.transport.on("loopStart", this._boundRestart);
|
||||||
|
this.transport.on("ticks", this._boundRestart);
|
||||||
this.context = this.transport.context;
|
this.context = this.transport.context;
|
||||||
this._restart();
|
this._restart();
|
||||||
}
|
}
|
||||||
|
@ -89,13 +91,25 @@ export class TransportRepeatEvent extends TransportEvent {
|
||||||
super.invoke(time);
|
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
|
* Push more events onto the timeline to keep up with the position of the timeline
|
||||||
*/
|
*/
|
||||||
private _createEvents(time: Seconds): void {
|
private _createEvents(time: Seconds): void {
|
||||||
// schedule the next event
|
// schedule the next event
|
||||||
const ticks = this.transport.getTicksAtTime(time);
|
// const ticks = this.transport.getTicksAtTime(time);
|
||||||
if (ticks >= this.time && ticks >= this._nextTick && this._nextTick + this._interval < this.time + this.duration) {
|
// 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._nextTick += this._interval;
|
||||||
this._currentId = this._nextId;
|
this._currentId = this._nextId;
|
||||||
this._nextId = this.transport.scheduleOnce(this.invoke.bind(this),
|
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 {
|
private _restart(time?: Time): void {
|
||||||
this.transport.clear(this._currentId);
|
this.transport.clear(this._currentId);
|
||||||
this.transport.clear(this._nextId);
|
this.transport.clear(this._nextId);
|
||||||
this._nextTick = this.time;
|
// start at the first event
|
||||||
|
this._nextTick = this.floatTime;
|
||||||
const ticks = this.transport.getTicksAtTime(time);
|
const ticks = this.transport.getTicksAtTime(time);
|
||||||
if (ticks > this.time) {
|
if (GT(ticks, this.time)) {
|
||||||
this._nextTick = this.time + Math.ceil((ticks - this.time) / this._interval) * this._interval;
|
// 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),
|
this._currentId = this._createEvent();
|
||||||
new TicksClass(this.context, this._nextTick).toSeconds());
|
|
||||||
this._nextTick += this._interval;
|
this._nextTick += this._interval;
|
||||||
this._nextId = this.transport.scheduleOnce(this.invoke.bind(this),
|
this._nextId = this._createEvent();
|
||||||
new TicksClass(this.context, this._nextTick).toSeconds());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,6 +144,7 @@ export class TransportRepeatEvent extends TransportEvent {
|
||||||
this.transport.clear(this._nextId);
|
this.transport.clear(this._nextId);
|
||||||
this.transport.off("start", this._boundRestart);
|
this.transport.off("start", this._boundRestart);
|
||||||
this.transport.off("loopStart", this._boundRestart);
|
this.transport.off("loopStart", this._boundRestart);
|
||||||
|
this.transport.off("ticks", this._boundRestart);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ export class TimeClass<Type extends Seconds | Ticks = Seconds, Unit extends stri
|
||||||
toTicks(): Ticks {
|
toTicks(): Ticks {
|
||||||
const quarterTime = this._beatsToUnits(1);
|
const quarterTime = this._beatsToUnits(1);
|
||||||
const quarters = this.valueOf() / quarterTime;
|
const quarters = this.valueOf() / quarterTime;
|
||||||
return Math.round(quarters * this._getPPQ());
|
return quarters * this._getPPQ();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -235,7 +235,7 @@ describe("Loop", () => {
|
||||||
}
|
}
|
||||||
}).start(0);
|
}).start(0);
|
||||||
transport.start();
|
transport.start();
|
||||||
}, 0.8).then(() => {
|
}, 0.81).then(() => {
|
||||||
expect(callCount).to.equal(9);
|
expect(callCount).to.equal(9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -326,7 +326,7 @@ describe("Loop", () => {
|
||||||
loop.playbackRate = 1.5;
|
loop.playbackRate = 1.5;
|
||||||
expect(loop.playbackRate).to.equal(1.5);
|
expect(loop.playbackRate).to.equal(1.5);
|
||||||
transport.start();
|
transport.start();
|
||||||
}, 0.8).then(() => {
|
}, 0.81).then(() => {
|
||||||
expect(callCount).to.equal(13);
|
expect(callCount).to.equal(13);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -355,11 +355,11 @@ describe("ToneEvent", () => {
|
||||||
|
|
||||||
it("can be started and stopped multiple times", () => {
|
it("can be started and stopped multiple times", () => {
|
||||||
return Offline(({ transport }) => {
|
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;
|
let eventTimeIndex = 0;
|
||||||
new ToneEvent({
|
new ToneEvent({
|
||||||
loop: true,
|
loop: true,
|
||||||
loopEnd: 0.1,
|
loopEnd: 0.09,
|
||||||
callback(time): void {
|
callback(time): void {
|
||||||
expect(eventTimes.length).to.be.gt(eventTimeIndex);
|
expect(eventTimes.length).to.be.gt(eventTimeIndex);
|
||||||
expect(eventTimes[eventTimeIndex]).to.be.closeTo(time, 0.05);
|
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") {
|
if (this._state.getValueAtTime(ticks) === "started") {
|
||||||
this._state.setStateAtTime("stopped", ticks, { id: -1 });
|
this._state.setStateAtTime("stopped", ticks, { id: -1 });
|
||||||
const previousEvent = this._state.getBefore(ticks);
|
const previousEvent = this._state.getBefore(ticks);
|
||||||
let reschedulTime = ticks;
|
let rescheduleTime = ticks;
|
||||||
if (previousEvent !== null) {
|
if (previousEvent !== null) {
|
||||||
reschedulTime = previousEvent.time;
|
rescheduleTime = previousEvent.time;
|
||||||
}
|
}
|
||||||
this._rescheduleEvents(reschedulTime);
|
this._rescheduleEvents(rescheduleTime);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -300,7 +300,7 @@ export class ToneEvent<ValueType = any> extends ToneWithContext<ToneEventOptions
|
||||||
* Get the duration of the loop.
|
* Get the duration of the loop.
|
||||||
*/
|
*/
|
||||||
protected _getLoopDuration(): Ticks {
|
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
|
* @example
|
||||||
* const note = new Tone.ToneEvent();
|
* const note = new Tone.ToneEvent();
|
||||||
* note.loop = true;
|
* note.loop = true;
|
||||||
|
|
|
@ -368,7 +368,7 @@ describe("Source", () => {
|
||||||
}, 0.7).then(output => {
|
}, 0.7).then(output => {
|
||||||
expect(output.getValueAtTime(0.01)).to.be.closeTo(0.2, 0.01);
|
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.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);
|
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",
|
"name": "tone",
|
||||||
"version": "14.7.0",
|
"version": "14.8.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tone",
|
"name": "tone",
|
||||||
"version": "14.7.0",
|
"version": "14.8.0",
|
||||||
"description": "A Web Audio framework for making interactive music in the browser.",
|
"description": "A Web Audio framework for making interactive music in the browser.",
|
||||||
"main": "build/Tone.js",
|
"main": "build/Tone.js",
|
||||||
"module": "build/esm/index.js",
|
"module": "build/esm/index.js",
|
||||||
|
|
Loading…
Reference in a new issue