2019-07-30 19:35:27 +00:00
|
|
|
import { TimeClass } from "../../core/type/Time";
|
2019-07-11 13:57:06 +00:00
|
|
|
import { PlaybackState } from "../../core/util/StateTimeline";
|
|
|
|
import { Signal } from "../../signal/Signal";
|
2019-07-16 19:41:59 +00:00
|
|
|
import { onContextClose, onContextInit } from "../context/ContextInitialization";
|
2019-05-22 03:37:03 +00:00
|
|
|
import { Gain } from "../context/Gain";
|
|
|
|
import { Param } from "../context/Param";
|
|
|
|
import { ToneWithContext, ToneWithContextOptions } from "../context/ToneWithContext";
|
|
|
|
import { TicksClass } from "../type/Ticks";
|
|
|
|
import { TransportTimeClass } from "../type/TransportTime";
|
2019-07-30 19:35:27 +00:00
|
|
|
import { BarsBeatsSixteenths, BPM, NormalRange, Seconds,
|
|
|
|
Subdivision, Ticks, Time, TimeSignature, TransportTime } from "../type/Units";
|
2019-05-22 03:37:03 +00:00
|
|
|
import { optionsFromArguments } from "../util/Defaults";
|
|
|
|
import { Emitter } from "../util/Emitter";
|
|
|
|
import { readOnly, writable } from "../util/Interface";
|
|
|
|
import { IntervalTimeline } from "../util/IntervalTimeline";
|
|
|
|
import { Timeline } from "../util/Timeline";
|
|
|
|
import { isArray, isDefined } from "../util/TypeCheck";
|
|
|
|
import { Clock } from "./Clock";
|
|
|
|
import { TransportEvent } from "./TransportEvent";
|
|
|
|
import { TransportRepeatEvent } from "./TransportRepeatEvent";
|
|
|
|
|
|
|
|
interface TransportOptions extends ToneWithContextOptions {
|
|
|
|
bpm: BPM;
|
|
|
|
swing: NormalRange;
|
|
|
|
swingSubdivision: Subdivision;
|
|
|
|
timeSignature: number;
|
|
|
|
loopStart: Time;
|
|
|
|
loopEnd: Time;
|
|
|
|
ppq: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
type TransportEventNames = "start" | "stop" | "pause" | "loop" | "loopEnd" | "loopStart";
|
|
|
|
|
|
|
|
interface SyncedSignalEvent {
|
|
|
|
signal: Signal;
|
|
|
|
initial: number;
|
|
|
|
ratio: Gain;
|
|
|
|
}
|
|
|
|
|
|
|
|
type TransportCallback = (time: Seconds) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transport for timing musical events.
|
|
|
|
* Supports tempo curves and time changes. Unlike browser-based timing (setInterval, requestAnimationFrame)
|
|
|
|
* Transport timing events pass in the exact time of the scheduled event
|
|
|
|
* in the argument of the callback function. Pass that time value to the object
|
|
|
|
* you're scheduling. <br><br>
|
|
|
|
* A single transport is created for you when the library is initialized.
|
|
|
|
* <br><br>
|
|
|
|
* The transport emits the events: "start", "stop", "pause", and "loop" which are
|
|
|
|
* called with the time of that event as the argument.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* //repeated event every 8th note
|
|
|
|
* Transport.scheduleRepeat(function(time){
|
|
|
|
* //do something with the time
|
|
|
|
* }, "8n");
|
|
|
|
* @example
|
|
|
|
* //schedule an event on the 16th measure
|
|
|
|
* Transport.schedule(function(time){
|
|
|
|
* //do something with the time
|
|
|
|
* }, "16:0:0");
|
2019-08-21 20:59:01 +00:00
|
|
|
* @category Core
|
2019-05-22 03:37:03 +00:00
|
|
|
*/
|
|
|
|
export class Transport extends ToneWithContext<TransportOptions> implements Emitter<TransportEventNames> {
|
|
|
|
|
|
|
|
name = "Transport";
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
// LOOPING
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If the transport loops or not.
|
|
|
|
*/
|
|
|
|
loop: boolean = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The loop start position in ticks
|
|
|
|
*/
|
|
|
|
private _loopStart: Ticks = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The loop end position in ticks
|
|
|
|
*/
|
|
|
|
private _loopEnd: Ticks = 0;
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
// CLOCK/TEMPO
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pulses per quarter is the number of ticks per quarter note.
|
|
|
|
*/
|
|
|
|
private _ppq: number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* watches the main oscillator for timing ticks
|
|
|
|
* initially starts at 120bpm
|
|
|
|
*/
|
2019-07-15 19:37:25 +00:00
|
|
|
private _clock: Clock<BPM>;
|
2019-05-22 03:37:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The Beats Per Minute of the Transport.
|
|
|
|
* @example
|
|
|
|
* Transport.bpm.value = 80;
|
|
|
|
* //ramp the bpm to 120 over 10 seconds
|
|
|
|
* Transport.bpm.rampTo(120, 10);
|
|
|
|
*/
|
2019-07-15 19:37:25 +00:00
|
|
|
bpm: Param<BPM>;
|
2019-05-22 03:37:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The time signature, or more accurately the numerator
|
|
|
|
* of the time signature over a denominator of 4.
|
|
|
|
*/
|
|
|
|
private _timeSignature: number;
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
// TIMELINE EVENTS
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* All the events in an object to keep track by ID
|
|
|
|
*/
|
|
|
|
private _scheduledEvents = {};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The scheduled events.
|
|
|
|
*/
|
|
|
|
private _timeline: Timeline<TransportEvent> = new Timeline();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Repeated events
|
|
|
|
*/
|
|
|
|
private _repeatedEvents: IntervalTimeline = new IntervalTimeline();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* All of the synced Signals
|
|
|
|
*/
|
|
|
|
private _syncedSignals: SyncedSignalEvent[] = [];
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
// SWING
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The subdivision of the swing
|
|
|
|
*/
|
|
|
|
private _swingTicks: Ticks;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The swing amount
|
|
|
|
*/
|
|
|
|
private _swingAmount: NormalRange = 0;
|
|
|
|
|
2019-06-23 19:02:38 +00:00
|
|
|
constructor(options?: Partial<TransportOptions>);
|
2019-05-22 03:37:03 +00:00
|
|
|
constructor() {
|
|
|
|
|
2019-06-23 19:02:38 +00:00
|
|
|
super(optionsFromArguments(Transport.getDefaults(), arguments));
|
|
|
|
const options = optionsFromArguments(Transport.getDefaults(), arguments);
|
2019-05-22 03:37:03 +00:00
|
|
|
|
|
|
|
// CLOCK/TEMPO
|
|
|
|
this._ppq = options.ppq;
|
|
|
|
this._clock = new Clock({
|
|
|
|
callback : this._processTick.bind(this),
|
2019-06-23 19:02:38 +00:00
|
|
|
context: this.context,
|
2019-05-22 03:37:03 +00:00
|
|
|
frequency : 0,
|
|
|
|
units: "bpm",
|
|
|
|
});
|
|
|
|
this._bindClockEvents();
|
|
|
|
this.bpm = this._clock.frequency;
|
|
|
|
this._clock.frequency.multiplier = options.ppq;
|
|
|
|
this.bpm.value = options.bpm;
|
|
|
|
readOnly(this, "bpm");
|
|
|
|
this._timeSignature = options.timeSignature;
|
|
|
|
|
|
|
|
// SWING
|
|
|
|
this._swingTicks = options.ppq / 2; // 8n
|
|
|
|
}
|
|
|
|
|
|
|
|
static getDefaults(): TransportOptions {
|
|
|
|
return Object.assign(ToneWithContext.getDefaults(), {
|
|
|
|
bpm: 120,
|
|
|
|
loopEnd: "4m" as Subdivision,
|
|
|
|
loopStart: 0,
|
|
|
|
ppq: 192,
|
|
|
|
swing: 0,
|
|
|
|
swingSubdivision: "8n" as Subdivision,
|
|
|
|
timeSignature: 4,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// TICKS
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* called on every tick
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param tickTime clock relative tick time
|
|
|
|
* @private
|
2019-05-22 03:37:03 +00:00
|
|
|
*/
|
|
|
|
private _processTick(tickTime: Seconds, ticks: Ticks): void {
|
|
|
|
// handle swing
|
|
|
|
if (this._swingAmount > 0 &&
|
|
|
|
ticks % this._ppq !== 0 && // not on a downbeat
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
// do the loop test
|
|
|
|
if (this.loop) {
|
|
|
|
if (ticks >= this._loopEnd) {
|
|
|
|
this.emit("loopEnd", tickTime);
|
|
|
|
this._clock.setTicksAtTime(this._loopStart, tickTime);
|
|
|
|
ticks = this._loopStart;
|
|
|
|
this.emit("loopStart", tickTime, this._clock.getSecondsAtTime(tickTime));
|
|
|
|
this.emit("loop", tickTime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// invoke the timeline events scheduled on this tick
|
|
|
|
this._timeline.forEachAtTime(ticks, event => event.invoke(tickTime));
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// SCHEDULABLE EVENTS
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedule an event along the timeline.
|
|
|
|
* @param callback The callback to be invoked at the time.
|
|
|
|
* @param time The time to invoke the callback at.
|
|
|
|
* @return The id of the event which can be used for canceling the event.
|
|
|
|
* @example
|
|
|
|
* //trigger the callback when the Transport reaches the desired time
|
|
|
|
* Transport.schedule(function(time){
|
|
|
|
* envelope.triggerAttack(time);
|
|
|
|
* }, "128i");
|
|
|
|
*/
|
|
|
|
schedule(callback: TransportCallback, time: TransportTime | TransportTimeClass): number {
|
|
|
|
const event = new TransportEvent(this, {
|
|
|
|
callback,
|
|
|
|
time : new TransportTimeClass(this.context, time).toTicks(),
|
|
|
|
});
|
|
|
|
return this._addEvent(event, this._timeline);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedule a repeated event along the timeline. The event will fire
|
|
|
|
* at the `interval` starting at the `startTime` and for the specified
|
|
|
|
* `duration`.
|
|
|
|
* @param callback The callback to invoke.
|
|
|
|
* @param interval The duration between successive callbacks. Must be a positive number.
|
|
|
|
* @param startTime When along the timeline the events should start being invoked.
|
|
|
|
* @param duration How long the event should repeat.
|
|
|
|
* @return The ID of the scheduled event. Use this to cancel the event.
|
|
|
|
* @example
|
|
|
|
* //a callback invoked every eighth note after the first measure
|
|
|
|
* Transport.scheduleRepeat(callback, "8n", "1m");
|
|
|
|
*/
|
|
|
|
scheduleRepeat(
|
|
|
|
callback: TransportCallback,
|
2019-07-23 17:12:27 +00:00
|
|
|
interval: Time | TimeClass,
|
2019-05-22 03:37:03 +00:00
|
|
|
startTime?: TransportTime | TransportTimeClass,
|
|
|
|
duration: Time = Infinity,
|
|
|
|
): number {
|
|
|
|
const event = new TransportRepeatEvent(this, {
|
|
|
|
callback,
|
|
|
|
duration : new TimeClass(this.context, duration).toTicks(),
|
|
|
|
interval: new TimeClass(this.context, interval).toTicks(),
|
|
|
|
time : new TransportTimeClass(this.context, startTime).toTicks(),
|
|
|
|
});
|
|
|
|
// kick it off if the Transport is started
|
|
|
|
// @ts-ignore
|
|
|
|
return this._addEvent(event, this._repeatedEvents);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Schedule an event that will be removed after it is invoked.
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param callback The callback to invoke once.
|
|
|
|
* @param time The time the callback should be invoked.
|
|
|
|
* @returns The ID of the scheduled event.
|
2019-05-22 03:37:03 +00:00
|
|
|
*/
|
|
|
|
scheduleOnce(callback: TransportCallback, time: TransportTime | TransportTimeClass): number {
|
|
|
|
const event = new TransportEvent(this, {
|
|
|
|
callback,
|
|
|
|
once : true,
|
|
|
|
time : new TransportTimeClass(this.context, time).toTicks(),
|
|
|
|
});
|
|
|
|
return this._addEvent(event, this._timeline);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear the passed in event id from the timeline
|
|
|
|
* @param eventId The id of the event.
|
|
|
|
*/
|
|
|
|
clear(eventId: number): this {
|
|
|
|
if (this._scheduledEvents.hasOwnProperty(eventId)) {
|
|
|
|
const item = this._scheduledEvents[eventId.toString()];
|
|
|
|
item.timeline.remove(item.event);
|
|
|
|
item.event.dispose();
|
|
|
|
delete this._scheduledEvents[eventId.toString()];
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an event to the correct timeline. Keep track of the
|
|
|
|
* timeline it was added to.
|
|
|
|
* @returns the event id which was just added
|
|
|
|
*/
|
|
|
|
private _addEvent(event: TransportEvent, timeline: Timeline<TransportEvent>): number {
|
|
|
|
this._scheduledEvents[event.id.toString()] = {
|
|
|
|
event,
|
|
|
|
timeline,
|
|
|
|
};
|
|
|
|
timeline.add(event);
|
|
|
|
return event.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove scheduled events from the timeline after
|
|
|
|
* the given time. Repeated events will be removed
|
|
|
|
* if their startTime is after the given time
|
|
|
|
* @param after Clear all events after this time.
|
|
|
|
*/
|
|
|
|
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));
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// START/STOP/PAUSE
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Bind start/stop/pause events from the clock and emit them.
|
|
|
|
*/
|
|
|
|
private _bindClockEvents(): void {
|
|
|
|
this._clock.on("start", (time, offset) => {
|
|
|
|
offset = new TicksClass(this.context, offset).toSeconds();
|
|
|
|
this.emit("start", time, offset);
|
|
|
|
});
|
|
|
|
|
|
|
|
this._clock.on("stop", (time) => {
|
|
|
|
this.emit("stop", time);
|
|
|
|
});
|
|
|
|
|
|
|
|
this._clock.on("pause", (time) => {
|
|
|
|
this.emit("pause", time);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the playback state of the source, either "started", "stopped", or "paused"
|
|
|
|
*/
|
|
|
|
get state(): PlaybackState {
|
|
|
|
return this._clock.getStateAtTime(this.now());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the transport and all sources synced to the transport.
|
|
|
|
* @param time The time when the transport should start.
|
|
|
|
* @param offset The timeline offset to start the transport.
|
|
|
|
* @example
|
|
|
|
* //start the transport in one second starting at beginning of the 5th measure.
|
|
|
|
* Transport.start("+1", "4:0:0");
|
|
|
|
*/
|
|
|
|
start(time?: Time, offset?: TransportTime): this {
|
|
|
|
let offsetTicks;
|
|
|
|
if (isDefined(offset)) {
|
|
|
|
offsetTicks = this.toTicks(offset);
|
|
|
|
}
|
|
|
|
// start the clock
|
|
|
|
this._clock.start(time, offsetTicks);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop the transport and all sources synced to the transport.
|
|
|
|
* @param time The time when the transport should stop.
|
|
|
|
* @example
|
|
|
|
* Transport.stop();
|
|
|
|
*/
|
|
|
|
stop(time?: Time): this {
|
|
|
|
this._clock.stop(time);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pause the transport and all sources synced to the transport.
|
|
|
|
*/
|
|
|
|
pause(time?: Time): this {
|
|
|
|
this._clock.pause(time);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle the current state of the transport. If it is
|
|
|
|
* started, it will stop it, otherwise it will start the Transport.
|
|
|
|
* @param time The time of the event
|
|
|
|
*/
|
|
|
|
toggle(time?: Time): this {
|
|
|
|
time = this.toSeconds(time);
|
|
|
|
if (this._clock.getStateAtTime(time) !== "started") {
|
|
|
|
this.start(time);
|
|
|
|
} else {
|
|
|
|
this.stop(time);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// SETTERS/GETTERS
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The time signature as just the numerator over 4.
|
|
|
|
* For example 4/4 would be just 4 and 6/8 would be 3.
|
|
|
|
* @example
|
|
|
|
* //common time
|
|
|
|
* Transport.timeSignature = 4;
|
|
|
|
* // 7/8
|
|
|
|
* Transport.timeSignature = [7, 8];
|
|
|
|
* //this will be reduced to a single number
|
|
|
|
* Transport.timeSignature; //returns 3.5
|
|
|
|
*/
|
|
|
|
get timeSignature(): TimeSignature {
|
|
|
|
return this._timeSignature;
|
|
|
|
}
|
|
|
|
set timeSignature(timeSig: TimeSignature) {
|
|
|
|
if (isArray(timeSig)) {
|
|
|
|
timeSig = (timeSig[0] / timeSig[1]) * 4;
|
|
|
|
}
|
|
|
|
this._timeSignature = timeSig;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the Transport.loop = true, this is the starting position of the loop.
|
|
|
|
*/
|
|
|
|
get loopStart(): Time {
|
2019-07-30 19:35:27 +00:00
|
|
|
return new TimeClass(this.context, this._loopStart, "i").toSeconds();
|
2019-05-22 03:37:03 +00:00
|
|
|
}
|
|
|
|
set loopStart(startPosition: Time) {
|
|
|
|
this._loopStart = this.toTicks(startPosition);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the Transport.loop = true, this is the ending position of the loop.
|
|
|
|
*/
|
|
|
|
get loopEnd(): Time {
|
2019-07-30 19:35:27 +00:00
|
|
|
return new TimeClass(this.context, this._loopEnd, "i").toSeconds();
|
2019-05-22 03:37:03 +00:00
|
|
|
}
|
|
|
|
set loopEnd(endPosition: Time) {
|
|
|
|
this._loopEnd = this.toTicks(endPosition);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the loop start and stop at the same time.
|
|
|
|
* @example
|
|
|
|
* //loop over the first measure
|
|
|
|
* Transport.setLoopPoints(0, "1m");
|
|
|
|
* Transport.loop = true;
|
|
|
|
*/
|
|
|
|
setLoopPoints(startPosition: TransportTime, endPosition: TransportTime): this {
|
|
|
|
this.loopStart = startPosition;
|
|
|
|
this.loopEnd = endPosition;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The swing value. Between 0-1 where 1 equal to the note + half the subdivision.
|
|
|
|
*/
|
|
|
|
get swing(): NormalRange {
|
|
|
|
return this._swingAmount;
|
|
|
|
}
|
|
|
|
set swing(amount: NormalRange) {
|
|
|
|
// scale the values to a normal range
|
|
|
|
this._swingAmount = amount;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the subdivision which the swing will be applied to.
|
|
|
|
* The default value is an 8th note. Value must be less
|
|
|
|
* than a quarter note.
|
|
|
|
*/
|
|
|
|
get swingSubdivision(): Subdivision {
|
|
|
|
return new TicksClass(this.context, this._swingTicks).toNotation();
|
|
|
|
}
|
|
|
|
set swingSubdivision(subdivision: Subdivision) {
|
|
|
|
this._swingTicks = this.toTicks(subdivision);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Transport's position in Bars:Beats:Sixteenths.
|
|
|
|
* Setting the value will jump to that position right away.
|
|
|
|
*/
|
|
|
|
get position(): BarsBeatsSixteenths | Time {
|
|
|
|
const now = this.now();
|
|
|
|
const ticks = this._clock.getTicksAtTime(now);
|
|
|
|
return new TicksClass(this.context, ticks).toBarsBeatsSixteenths();
|
|
|
|
}
|
|
|
|
set position(progress: Time) {
|
|
|
|
const ticks = this.toTicks(progress);
|
|
|
|
this.ticks = ticks;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Transport's position in seconds
|
|
|
|
* Setting the value will jump to that position right away.
|
|
|
|
*/
|
|
|
|
get seconds(): Seconds {
|
|
|
|
return this._clock.seconds;
|
|
|
|
}
|
|
|
|
|
|
|
|
set seconds(s: Seconds) {
|
|
|
|
const now = this.now();
|
|
|
|
const ticks = this._clock.frequency.timeToTicks(s, now);
|
|
|
|
this.ticks = ticks;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Transport's loop position as a normalized value. Always
|
|
|
|
* returns 0 if the transport if loop is not true.
|
|
|
|
*/
|
|
|
|
get progress(): NormalRange {
|
|
|
|
if (this.loop) {
|
|
|
|
const now = this.now();
|
|
|
|
const ticks = this._clock.getTicksAtTime(now);
|
|
|
|
return (ticks - this._loopStart) / (this._loopEnd - this._loopStart);
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The transports current tick position.
|
|
|
|
*/
|
|
|
|
get ticks(): Ticks {
|
|
|
|
return this._clock.ticks;
|
|
|
|
}
|
|
|
|
set ticks(t: Ticks) {
|
|
|
|
if (this._clock.ticks !== t) {
|
|
|
|
const now = this.now();
|
|
|
|
// stop everything synced to the transport
|
|
|
|
if (this.state === "started") {
|
|
|
|
this.emit("stop", now);
|
|
|
|
this._clock.setTicksAtTime(t, now);
|
|
|
|
// restart it with the new time
|
|
|
|
this.emit("start", now, this.seconds);
|
|
|
|
} else {
|
|
|
|
this._clock.setTicksAtTime(t, now);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the clock's ticks at the given time.
|
|
|
|
* @param time When to get the tick value
|
|
|
|
* @return The tick value at the given time.
|
|
|
|
*/
|
2019-07-30 14:25:17 +00:00
|
|
|
getTicksAtTime(time?: Time): Ticks {
|
2019-05-22 03:37:03 +00:00
|
|
|
return Math.round(this._clock.getTicksAtTime(time));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the elapsed seconds at the given time.
|
2019-08-30 16:06:38 +00:00
|
|
|
* @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 {
|
|
|
|
return this._clock.getSecondsAtTime(time);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pulses Per Quarter note. This is the smallest resolution
|
|
|
|
* the Transport timing supports. This should be set once
|
|
|
|
* on initialization and not set again. Changing this value
|
|
|
|
* after other objects have been created can cause problems.
|
|
|
|
*/
|
|
|
|
get PPQ(): number {
|
|
|
|
return this._clock.frequency.multiplier;
|
|
|
|
}
|
|
|
|
set PPQ(ppq: number) {
|
|
|
|
this._clock.frequency.multiplier = ppq;
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// SYNCING
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the time aligned to the next subdivision
|
|
|
|
* of the Transport. If the Transport is not started,
|
|
|
|
* it will return 0.
|
|
|
|
* Note: this will not work precisely during tempo ramps.
|
|
|
|
* @param subdivision The subdivision to quantize to
|
|
|
|
* @return The context time of the next subdivision.
|
|
|
|
* @example
|
|
|
|
* Transport.start(); //the transport must be started
|
|
|
|
* Transport.nextSubdivision("4n");
|
|
|
|
*/
|
|
|
|
nextSubdivision(subdivision?: Time): Seconds {
|
|
|
|
subdivision = this.toTicks(subdivision);
|
|
|
|
if (this.state !== "started") {
|
|
|
|
// if the transport's not started, return 0
|
|
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
const now = this.now();
|
|
|
|
// the remainder of the current ticks and the subdivision
|
|
|
|
const transportPos = this.getTicksAtTime(now);
|
|
|
|
const remainingTicks = subdivision - transportPos % subdivision;
|
|
|
|
return this._clock.nextTickTime(remainingTicks, now);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attaches the signal to the tempo control signal so that
|
|
|
|
* any changes in the tempo will change the signal in the same
|
|
|
|
* ratio.
|
|
|
|
*
|
2019-08-30 16:06:38 +00:00
|
|
|
* @param signal
|
|
|
|
* @param ratio Optionally pass in the ratio between the two signals.
|
2019-05-23 18:00:49 +00:00
|
|
|
* Otherwise it will be computed based on their current values.
|
|
|
|
*/
|
2019-07-11 03:33:36 +00:00
|
|
|
syncSignal(signal: Signal<any>, ratio?: number): this {
|
2019-05-23 18:00:49 +00:00
|
|
|
if (!ratio) {
|
|
|
|
// get the sync ratio
|
|
|
|
const now = this.now();
|
|
|
|
if (signal.getValueAtTime(now) !== 0) {
|
2019-07-11 03:33:36 +00:00
|
|
|
const bpm = this.bpm.getValueAtTime(now);
|
|
|
|
const computedFreq = 1 / (60 / bpm / this.PPQ);
|
|
|
|
ratio = signal.getValueAtTime(now) / computedFreq;
|
2019-05-23 18:00:49 +00:00
|
|
|
} else {
|
|
|
|
ratio = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const ratioSignal = new Gain(ratio);
|
2019-07-11 03:33:36 +00:00
|
|
|
// @ts-ignore
|
|
|
|
this.bpm.connect(ratioSignal);
|
|
|
|
// @ts-ignore
|
|
|
|
ratioSignal.connect(signal._param);
|
2019-05-23 18:00:49 +00:00
|
|
|
this._syncedSignals.push({
|
|
|
|
initial : signal.value,
|
|
|
|
ratio : ratioSignal,
|
|
|
|
signal,
|
|
|
|
});
|
|
|
|
signal.value = 0;
|
|
|
|
return this;
|
2019-05-22 03:37:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unsyncs a previously synced signal from the transport's control.
|
|
|
|
* See Transport.syncSignal.
|
|
|
|
*/
|
2019-07-11 03:33:36 +00:00
|
|
|
unsyncSignal(signal: Signal<any>): this {
|
2019-05-23 18:00:49 +00:00
|
|
|
for (let i = this._syncedSignals.length - 1; i >= 0; i--) {
|
|
|
|
const syncedSignal = this._syncedSignals[i];
|
|
|
|
if (syncedSignal.signal === signal) {
|
|
|
|
syncedSignal.ratio.dispose();
|
|
|
|
syncedSignal.signal.value = syncedSignal.initial;
|
|
|
|
this._syncedSignals.splice(i, 1);
|
|
|
|
}
|
|
|
|
}
|
2019-05-22 03:37:03 +00:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up.
|
|
|
|
*/
|
|
|
|
dispose(): this {
|
2019-07-23 16:11:57 +00:00
|
|
|
super.dispose();
|
2019-05-22 03:37:03 +00:00
|
|
|
this._clock.dispose();
|
|
|
|
writable(this, "bpm");
|
|
|
|
this._timeline.dispose();
|
|
|
|
this._repeatedEvents.dispose();
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////
|
|
|
|
// 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;
|
|
|
|
emit!: (event: any, ...args: any[]) => this;
|
|
|
|
}
|
|
|
|
|
|
|
|
Emitter.mixin(Transport);
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// INITIALIZATION
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2019-07-11 13:57:06 +00:00
|
|
|
onContextInit(context => {
|
2019-06-23 19:02:38 +00:00
|
|
|
context.transport = new Transport({ context });
|
2019-06-24 17:41:38 +00:00
|
|
|
});
|
2019-07-16 19:41:59 +00:00
|
|
|
|
|
|
|
onContextClose(context => {
|
|
|
|
context.transport.dispose();
|
|
|
|
});
|