warn if event is scheduled without using the scheduled time.

addresses #959
This commit is contained in:
Yotam Mann 2021-10-13 19:03:14 -04:00
parent 52cf924ee7
commit 6dd22e752f
4 changed files with 147 additions and 34 deletions

View file

@ -7,6 +7,8 @@ import { TransportTime } from "../type/TransportTime";
import { Transport } from "./Transport";
// importing for side affects
import "../context/Destination";
import { warns } from "test/helper/Basic";
import { Synth } from "Tone/instrument/Synth";
describe("Transport", () => {
@ -546,6 +548,19 @@ describe("Transport", () => {
});
});
it("warns if the scheduled time was not used in the callback", async () => {
return Offline(({ transport }) => {
const synth = new Synth();
transport.schedule(() => {
warns(() => {
synth.triggerAttackRelease("C2", 0.1);
});
}, 0);
transport.start(0);
}, 0.3).then(() => {
});
});
});
context("scheduleRepeat", () => {

View file

@ -2,15 +2,29 @@ import { TimeClass } from "../../core/type/Time";
import { PlaybackState } from "../../core/util/StateTimeline";
import { TimelineValue } from "../../core/util/TimelineValue";
import { Signal } from "../../signal/Signal";
import { onContextClose, onContextInit } from "../context/ContextInitialization";
import {
onContextClose,
onContextInit,
} from "../context/ContextInitialization";
import { Gain } from "../context/Gain";
import { ToneWithContext, ToneWithContextOptions } from "../context/ToneWithContext";
import {
ToneWithContext,
ToneWithContextOptions,
} from "../context/ToneWithContext";
import { TicksClass } from "../type/Ticks";
import { TransportTimeClass } from "../type/TransportTime";
import {
BarsBeatsSixteenths, BPM, NormalRange, Seconds,
Subdivision, Ticks, Time, TimeSignature, TransportTime
BarsBeatsSixteenths,
BPM,
NormalRange,
Seconds,
Subdivision,
Ticks,
Time,
TimeSignature,
TransportTime,
} from "../type/Units";
import { enterScheduledCallback } from "../util/Debug";
import { optionsFromArguments } from "../util/Defaults";
import { Emitter } from "../util/Emitter";
import { readOnly, writable } from "../util/Interface";
@ -32,7 +46,14 @@ interface TransportOptions extends ToneWithContextOptions {
ppq: number;
}
type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart" | "ticks";
type TransportEventNames =
| "start"
| "stop"
| "pause"
| "loop"
| "loopEnd"
| "loopStart"
| "ticks";
interface SyncedSignalEvent {
signal: Signal;
@ -64,8 +85,9 @@ type TransportCallback = (time: Seconds) => void;
* Tone.Transport.start();
* @category Core
*/
export class Transport extends ToneWithContext<TransportOptions> implements Emitter<TransportEventNames> {
export class Transport
extends ToneWithContext<TransportOptions>
implements Emitter<TransportEventNames> {
readonly name: string = "Transport";
//-------------------------------------
@ -163,9 +185,11 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
constructor(options?: Partial<TransportOptions>);
constructor() {
super(optionsFromArguments(Transport.getDefaults(), arguments));
const options = optionsFromArguments(Transport.getDefaults(), arguments);
const options = optionsFromArguments(
Transport.getDefaults(),
arguments
);
// CLOCK/TEMPO
this._ppq = options.ppq;
@ -213,21 +237,34 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
this.emit("loopEnd", tickTime);
this._clock.setTicksAtTime(this._loopStart, tickTime);
ticks = this._loopStart;
this.emit("loopStart", tickTime, this._clock.getSecondsAtTime(tickTime));
this.emit(
"loopStart",
tickTime,
this._clock.getSecondsAtTime(tickTime)
);
this.emit("loop", tickTime);
}
}
// handle swing
if (this._swingAmount > 0 &&
if (
this._swingAmount > 0 &&
ticks % this._ppq !== 0 && // not on a downbeat
ticks % (this._swingTicks * 2) !== 0) {
ticks % (this._swingTicks * 2) !== 0
) {
// add some swing
const progress = (ticks % (this._swingTicks * 2)) / (this._swingTicks * 2);
const amount = Math.sin((progress) * Math.PI) * this._swingAmount;
tickTime += new TicksClass(this.context, this._swingTicks * 2 / 3).toSeconds() * amount;
const progress =
(ticks % (this._swingTicks * 2)) / (this._swingTicks * 2);
const amount = Math.sin(progress * Math.PI) * this._swingAmount;
tickTime +=
new TicksClass(
this.context,
(this._swingTicks * 2) / 3
).toSeconds() * amount;
}
// invoke the timeline events scheduled on this tick
this._timeline.forEachAtTime(ticks, event => event.invoke(tickTime));
enterScheduledCallback(true);
this._timeline.forEachAtTime(ticks, (event) => event.invoke(tickTime));
enterScheduledCallback(false);
}
//-------------------------------------
@ -246,7 +283,10 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
* console.log("measure 16!");
* }, "16:0:0");
*/
schedule(callback: TransportCallback, time: TransportTime | TransportTimeClass): number {
schedule(
callback: TransportCallback,
time: TransportTime | TransportTimeClass
): number {
const event = new TransportEvent(this, {
callback,
time: new TransportTimeClass(this.context, time).toTicks(),
@ -274,7 +314,7 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
callback: TransportCallback,
interval: Time | TimeClass,
startTime?: TransportTime | TransportTimeClass,
duration: Time = Infinity,
duration: Time = Infinity
): number {
const event = new TransportRepeatEvent(this, {
callback,
@ -293,7 +333,10 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
* @param time The time the callback should be invoked.
* @returns The ID of the scheduled event.
*/
scheduleOnce(callback: TransportCallback, time: TransportTime | TransportTimeClass): number {
scheduleOnce(
callback: TransportCallback,
time: TransportTime | TransportTimeClass
): number {
const event = new TransportEvent(this, {
callback,
once: true,
@ -338,8 +381,12 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
*/
cancel(after: TransportTime = 0): this {
const computedAfter = this.toTicks(after);
this._timeline.forEachFrom(computedAfter, event => this.clear(event.id));
this._repeatedEvents.forEachFrom(computedAfter, event => this.clear(event.id));
this._timeline.forEachFrom(computedAfter, (event) =>
this.clear(event.id)
);
this._repeatedEvents.forEachFrom(computedAfter, (event) =>
this.clear(event.id)
);
return this;
}
@ -381,6 +428,8 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
* Tone.Transport.start("+1", "4:0:0");
*/
start(time?: Time, offset?: TransportTime): this {
// start the context
this.context.resume();
let offsetTicks;
if (isDefined(offset)) {
offsetTicks = this.toTicks(offset);
@ -486,7 +535,10 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
* Tone.Transport.setLoopPoints(0, "1m");
* Tone.Transport.loop = true;
*/
setLoopPoints(startPosition: TransportTime, endPosition: TransportTime): this {
setLoopPoints(
startPosition: TransportTime,
endPosition: TransportTime
): this {
this.loopStart = startPosition;
this.loopEnd = endPosition;
return this;
@ -550,7 +602,9 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
if (this.loop) {
const now = this.now();
const ticks = this._clock.getTicksAtTime(now);
return (ticks - this._loopStart) / (this._loopEnd - this._loopStart);
return (
(ticks - this._loopStart) / (this._loopEnd - this._loopStart)
);
} else {
return 0;
}
@ -626,7 +680,7 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
* @return The context time of the next subdivision.
* @example
* // the transport must be started, otherwise returns 0
* Tone.Transport.start();
* Tone.Transport.start();
* Tone.Transport.nextSubdivision("4n");
*/
nextSubdivision(subdivision?: Time): Seconds {
@ -638,7 +692,7 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
const now = this.now();
// the remainder of the current ticks and the subdivision
const transportPos = this.getTicksAtTime(now);
const remainingTicks = subdivision - transportPos % subdivision;
const remainingTicks = subdivision - (transportPos % subdivision);
return this._clock.nextTickTime(remainingTicks, now);
}
}
@ -710,9 +764,18 @@ export class Transport extends ToneWithContext<TransportOptions> implements Emit
// EMITTER MIXIN TO SATISFY COMPILER
//-------------------------------------
on!: (event: TransportEventNames, callback: (...args: any[]) => void) => this;
once!: (event: TransportEventNames, callback: (...args: any[]) => void) => this;
off!: (event: TransportEventNames, callback?: ((...args: any[]) => void) | undefined) => this;
on!: (
event: TransportEventNames,
callback: (...args: any[]) => void
) => this;
once!: (
event: TransportEventNames,
callback: (...args: any[]) => void
) => this;
off!: (
event: TransportEventNames,
callback?: ((...args: any[]) => void) | undefined
) => this;
emit!: (event: any, ...args: any[]) => this;
}
@ -722,10 +785,10 @@ Emitter.mixin(Transport);
// INITIALIZATION
//-------------------------------------
onContextInit(context => {
onContextInit((context) => {
context.transport = new Transport({ context });
});
onContextClose(context => {
onContextClose((context) => {
context.transport.dispose();
});

View file

@ -4,6 +4,7 @@ import { FrequencyClass } from "../type/Frequency";
import { TimeClass } from "../type/Time";
import { TransportTimeClass } from "../type/TransportTime";
import { Frequency, Hertz, Seconds, Ticks, Time } from "../type/Units";
import { assertUsedScheduleTime } from "../util/Debug";
import { getDefaultsFromInstance, optionsFromArguments } from "../util/Defaults";
import { RecursivePartial } from "../util/Interface";
import { isArray, isBoolean, isDefined, isNumber, isString, isUndef } from "../util/TypeCheck";
@ -104,6 +105,7 @@ export abstract class ToneWithContext<Options extends ToneWithContextOptions> ex
* Tone.getTransport().bpm.rampTo(60, 30);
*/
toSeconds(time?: Time): Seconds {
assertUsedScheduleTime(time);
return new TimeClass(this.context, time).toSeconds();
}

View file

@ -1,3 +1,5 @@
import { isUndef } from "./TypeCheck";
/**
* Assert that the statement is true, otherwise invoke the error.
* @param statement
@ -14,17 +16,48 @@ export function assert(statement: boolean, error: string): asserts statement {
*/
export function assertRange(value: number, gte: number, lte = Infinity): void {
if (!(gte <= value && value <= lte)) {
throw new RangeError(`Value must be within [${gte}, ${lte}], got: ${value}`);
throw new RangeError(
`Value must be within [${gte}, ${lte}], got: ${value}`
);
}
}
/**
* Make sure that the given value is within the range
* Warn if the context is not running.
*/
export function assertContextRunning(context: import("../context/BaseContext").BaseContext): void {
export function assertContextRunning(
context: import("../context/BaseContext").BaseContext
): void {
// add a warning if the context is not started
if (!context.isOffline && context.state !== "running") {
warn("The AudioContext is \"suspended\". Invoke Tone.start() from a user action to start the audio.");
warn(
"The AudioContext is \"suspended\". Invoke Tone.start() from a user action to start the audio."
);
}
}
/**
* If it is currently inside a scheduled callback
*/
let isInsideScheduledCallback = false;
let printedScheduledWarning = false;
/**
* Notify that the following block of code is occurring inside a Transport callback.
*/
export function enterScheduledCallback(insideCallback: boolean): void {
isInsideScheduledCallback = insideCallback;
}
/**
* Make sure that a time was passed into
*/
export function assertUsedScheduleTime(
time?: import("../type/Units").Time
): void {
if (isUndef(time) && isInsideScheduledCallback && !printedScheduledWarning) {
printedScheduledWarning = true;
warn("Events scheduled inside of scheduled callbacks should use the passed in scheduling time. See https://github.com/Tonejs/Tone.js/wiki/Accurate-Timing");
}
}