Tone.js/Tone/core/clock/TickSource.ts

339 lines
10 KiB
TypeScript
Raw Normal View History

2019-05-22 03:37:03 +00:00
import { ToneWithContext, ToneWithContextOptions } from "../context/ToneWithContext";
2019-10-31 19:43:16 +00:00
import { Seconds, Ticks, Time } from "../type/Units";
2019-05-22 03:37:03 +00:00
import { optionsFromArguments } from "../util/Defaults";
import { readOnly } from "../util/Interface";
import { PlaybackState, StateTimeline, StateTimelineEvent } from "../util/StateTimeline";
import { Timeline } from "../util/Timeline";
2019-07-11 04:14:53 +00:00
import { isDefined } from "../util/TypeCheck";
2019-05-22 03:37:03 +00:00
import { TickSignal } from "./TickSignal";
import { EQ } from "../util/Math";
2019-05-22 03:37:03 +00:00
interface TickSourceOptions extends ToneWithContextOptions {
frequency: number;
units: "bpm" | "hertz";
2019-05-22 03:37:03 +00:00
}
interface TickSourceOffsetEvent {
ticks: number;
time: number;
seconds: number;
}
/**
2019-08-21 20:59:01 +00:00
* Uses [TickSignal](TickSignal) to track elapsed ticks with complex automation curves.
2019-05-22 03:37:03 +00:00
*/
export class TickSource<TypeName extends "bpm" | "hertz"> extends ToneWithContext<TickSourceOptions> {
2019-05-22 03:37:03 +00:00
2019-09-04 23:18:44 +00:00
readonly name: string = "TickSource";
2019-05-22 03:37:03 +00:00
/**
2019-09-14 20:39:18 +00:00
* The frequency the callback function should be invoked.
2019-05-22 03:37:03 +00:00
*/
readonly frequency: TickSignal<TypeName>;
2019-05-22 03:37:03 +00:00
/**
2019-09-14 20:39:18 +00:00
* The state timeline
2019-05-22 03:37:03 +00:00
*/
private _state: StateTimeline = new StateTimeline();
/**
* The offset values of the ticks
*/
private _tickOffset: Timeline<TickSourceOffsetEvent> = new Timeline();
2019-08-21 20:59:01 +00:00
/**
* @param frequency The initial frequency that the signal ticks at
*/
constructor(frequency?: number);
2019-05-22 03:37:03 +00:00
constructor(options?: Partial<TickSourceOptions>);
constructor() {
super(optionsFromArguments(TickSource.getDefaults(), arguments, ["frequency"]));
const options = optionsFromArguments(TickSource.getDefaults(), arguments, ["frequency"]);
this.frequency = new TickSignal({
2019-05-22 03:37:03 +00:00
context: this.context,
units: options.units as TypeName,
value: options.frequency,
2019-05-22 03:37:03 +00:00
});
readOnly(this, "frequency");
// set the initial state
2019-05-22 03:37:03 +00:00
this._state.setStateAtTime("stopped", 0);
// add the first event
this.setTicksAtTime(0, 0);
}
static getDefaults(): TickSourceOptions {
return Object.assign({
frequency: 1,
2019-07-19 16:32:55 +00:00
units: "hertz" as "hertz",
}, ToneWithContext.getDefaults());
2019-05-22 03:37:03 +00:00
}
/**
2019-09-14 20:39:18 +00:00
* Returns the playback state of the source, either "started", "stopped" or "paused".
2019-05-22 03:37:03 +00:00
*/
get state(): PlaybackState {
return this.getStateAtTime(this.now());
2019-05-22 03:37:03 +00:00
}
/**
2019-09-14 20:39:18 +00:00
* Start the clock at the given time. Optionally pass in an offset
* of where to start the tick counter from.
* @param time The time the clock should start
* @param offset The number of ticks to start the source at
2019-05-22 03:37:03 +00:00
*/
start(time: Time, offset?: Ticks): this {
const computedTime = this.toSeconds(time);
if (this._state.getValueAtTime(computedTime) !== "started") {
this._state.setStateAtTime("started", computedTime);
if (isDefined(offset)) {
this.setTicksAtTime(offset, computedTime);
}
}
return this;
}
/**
* Stop the clock. Stopping the clock resets the tick counter to 0.
* @param time The time when the clock should stop.
*/
stop(time: Time): this {
const computedTime = this.toSeconds(time);
// cancel the previous stop
if (this._state.getValueAtTime(computedTime) === "stopped") {
const event = this._state.get(computedTime);
if (event && event.time > 0) {
this._tickOffset.cancel(event.time);
this._state.cancel(event.time);
}
}
this._state.cancel(computedTime);
this._state.setStateAtTime("stopped", computedTime);
this.setTicksAtTime(0, computedTime);
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Pause the clock. Pausing does not reset the tick counter.
* @param time The time when the clock should stop.
2019-05-22 03:37:03 +00:00
*/
pause(time: Time): this {
const computedTime = this.toSeconds(time);
if (this._state.getValueAtTime(computedTime) === "started") {
this._state.setStateAtTime("paused", computedTime);
}
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Cancel start/stop/pause and setTickAtTime events scheduled after the given time.
* @param time When to clear the events after
2019-05-22 03:37:03 +00:00
*/
cancel(time: Time): this {
time = this.toSeconds(time);
this._state.cancel(time);
this._tickOffset.cancel(time);
return this;
}
/**
* Get the elapsed ticks at the given time
* @param time When to get the tick value
* @return The number of ticks
*/
2019-07-30 14:25:17 +00:00
getTicksAtTime(time?: Time): Ticks {
2019-05-22 03:37:03 +00:00
const computedTime = this.toSeconds(time);
const stopEvent = this._state.getLastState("stopped", computedTime) as StateTimelineEvent;
2019-05-22 03:37:03 +00:00
// this event allows forEachBetween to iterate until the current time
2019-09-16 03:32:40 +00:00
const tmpEvent: StateTimelineEvent = { state: "paused", time: computedTime };
2019-05-22 03:37:03 +00:00
this._state.add(tmpEvent);
// keep track of the previous offset event
let lastState = stopEvent;
let elapsedTicks = 0;
// iterate through all the events since the last stop
this._state.forEachBetween(stopEvent.time, computedTime + this.sampleTime, e => {
let periodStartTime = lastState.time;
// if there is an offset event in this period use that
const offsetEvent = this._tickOffset.get(e.time);
if (offsetEvent && offsetEvent.time >= lastState.time) {
elapsedTicks = offsetEvent.ticks;
periodStartTime = offsetEvent.time;
}
if (lastState.state === "started" && e.state !== "started") {
elapsedTicks += this.frequency.getTicksAtTime(e.time) - this.frequency.getTicksAtTime(periodStartTime);
}
lastState = e;
});
// remove the temporary event
this._state.remove(tmpEvent);
// return the ticks
return elapsedTicks;
}
/**
2019-09-14 20:39:18 +00:00
* The number of times the callback was invoked. Starts counting at 0
* and increments after the callback was invoked. Returns -1 when stopped.
2019-05-22 03:37:03 +00:00
*/
get ticks(): Ticks {
return this.getTicksAtTime(this.now());
}
set ticks(t: Ticks) {
this.setTicksAtTime(t, this.now());
}
/**
2019-09-14 20:39:18 +00:00
* The time since ticks=0 that the TickSource has been running. Accounts
* for tempo curves
2019-05-22 03:37:03 +00:00
*/
get seconds(): Seconds {
return this.getSecondsAtTime(this.now());
}
set seconds(s: Seconds) {
const now = this.now();
const ticks = this.frequency.timeToTicks(s, now);
this.setTicksAtTime(ticks, now);
}
/**
2019-09-14 20:39:18 +00:00
* Return the elapsed seconds at the given time.
* @param time When to get the elapsed seconds
* @return The number of elapsed seconds
2019-05-22 03:37:03 +00:00
*/
getSecondsAtTime(time: Time): Seconds {
time = this.toSeconds(time);
const stopEvent = this._state.getLastState("stopped", time) as StateTimelineEvent;
2019-05-22 03:37:03 +00:00
// this event allows forEachBetween to iterate until the current time
2019-09-16 03:32:40 +00:00
const tmpEvent: StateTimelineEvent = { state: "paused", time };
2019-05-22 03:37:03 +00:00
this._state.add(tmpEvent);
// keep track of the previous offset event
let lastState = stopEvent;
let elapsedSeconds = 0;
// iterate through all the events since the last stop
this._state.forEachBetween(stopEvent.time, time + this.sampleTime, e => {
let periodStartTime = lastState.time;
// if there is an offset event in this period use that
const offsetEvent = this._tickOffset.get(e.time);
if (offsetEvent && offsetEvent.time >= lastState.time) {
elapsedSeconds = offsetEvent.seconds;
periodStartTime = offsetEvent.time;
}
if (lastState.state === "started" && e.state !== "started") {
elapsedSeconds += e.time - periodStartTime;
}
lastState = e;
});
// remove the temporary event
this._state.remove(tmpEvent);
// return the ticks
return elapsedSeconds;
}
/**
* Set the clock's ticks at the given time.
* @param ticks The tick value to set
* @param time When to set the tick value
*/
setTicksAtTime(ticks: Ticks, time: Time): this {
time = this.toSeconds(time);
this._tickOffset.cancel(time);
this._tickOffset.add({
2019-09-16 03:32:40 +00:00
seconds: this.frequency.getDurationOfTicks(ticks, time),
2019-05-22 03:37:03 +00:00
ticks,
time,
});
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Returns the scheduled state at the given time.
* @param time The time to query.
2019-05-22 03:37:03 +00:00
*/
2019-08-19 16:59:31 +00:00
getStateAtTime(time: Time): PlaybackState {
2019-05-22 03:37:03 +00:00
time = this.toSeconds(time);
return this._state.getValueAtTime(time);
}
/**
* Get the time of the given tick. The second argument
* is when to test before. Since ticks can be set (with setTicksAtTime)
* there may be multiple times for a given tick value.
2019-10-23 03:04:52 +00:00
* @param tick The tick number.
2019-05-22 03:37:03 +00:00
* @param before When to measure the tick value from.
* @return The time of the tick
*/
getTimeOfTick(tick: Ticks, before = this.now()): Seconds {
const offset = this._tickOffset.get(before) as TickSourceOffsetEvent;
const event = this._state.get(before) as StateTimelineEvent;
const startTime = Math.max(offset.time, event.time);
const absoluteTicks = this.frequency.getTicksAtTime(startTime) + tick - offset.ticks;
return this.frequency.getTimeOfTick(absoluteTicks);
}
/**
2019-09-14 20:39:18 +00:00
* Invoke the callback event at all scheduled ticks between the
* start time and the end time
* @param startTime The beginning of the search range
* @param endTime The end of the search range
* @param callback The callback to invoke with each tick
2019-05-22 03:37:03 +00:00
*/
2019-11-12 19:12:22 +00:00
forEachTickBetween(startTime: number, endTime: number, callback: (when: Seconds, ticks: Ticks) => void): this {
2019-05-22 03:37:03 +00:00
// only iterate through the sections where it is "started"
2019-11-12 19:12:22 +00:00
let lastStateEvent = this._state.get(startTime);
this._state.forEachBetween(startTime, endTime, event => {
2019-05-22 03:37:03 +00:00
if (lastStateEvent && lastStateEvent.state === "started" && event.state !== "started") {
2019-11-12 19:12:22 +00:00
this.forEachTickBetween(Math.max(lastStateEvent.time, startTime), event.time - this.sampleTime, callback);
2019-05-22 03:37:03 +00:00
}
lastStateEvent = event;
});
2019-11-13 04:59:41 +00:00
let error: Error | null = null;
2019-05-22 03:37:03 +00:00
if (lastStateEvent && lastStateEvent.state === "started") {
2019-11-12 19:12:22 +00:00
const maxStartTime = Math.max(lastStateEvent.time, startTime);
2019-05-22 03:37:03 +00:00
// figure out the difference between the frequency ticks and the
const startTicks = this.frequency.getTicksAtTime(maxStartTime);
const ticksAtStart = this.frequency.getTicksAtTime(lastStateEvent.time);
const diff = startTicks - ticksAtStart;
let offset = Math.ceil(diff) - diff;
// guard against floating point issues
offset = EQ(offset, 1) ? 0 : offset;
2019-05-22 03:37:03 +00:00
let nextTickTime = this.frequency.getTimeOfTick(startTicks + offset);
while (nextTickTime < endTime) {
2019-05-22 03:37:03 +00:00
try {
callback(nextTickTime, Math.round(this.getTicksAtTime(nextTickTime)));
} catch (e) {
error = e;
break;
}
nextTickTime += this.frequency.getDurationOfTicks(1, nextTickTime);
2019-05-22 03:37:03 +00:00
}
}
if (error) {
throw error;
}
return this;
}
/**
2019-09-14 20:39:18 +00:00
* Clean up
2019-05-22 03:37:03 +00:00
*/
dispose(): this {
super.dispose();
2019-05-22 03:37:03 +00:00
this._state.dispose();
this._tickOffset.dispose();
this.frequency.dispose();
return this;
}
}