tone
Version:
A Web Audio framework for making interactive music in the browser.
654 lines • 23.9 kB
JavaScript
import * as tslib_1 from "tslib";
import { TimeClass } from "../../core/type/Time";
import { TimelineValue } from "../../core/util/TimelineValue";
import { onContextClose, onContextInit } from "../context/ContextInitialization";
import { Gain } from "../context/Gain";
import { ToneWithContext } from "../context/ToneWithContext";
import { TicksClass } from "../type/Ticks";
import { TransportTimeClass } from "../type/TransportTime";
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";
/**
* 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
* import { Oscillator, Transport } from "tone";
* const osc = new Oscillator().toDestination();
* // repeated event every 8th note
* Transport.scheduleRepeat((time) => {
* // use the callback time to schedule events
* osc.start(time).stop(time + 0.1);
* }, "8n");
* // transport must be started before it starts invoking events
* Transport.start();
* @category Core
*/
var Transport = /** @class */ (function (_super) {
tslib_1.__extends(Transport, _super);
function Transport() {
var _this = _super.call(this, optionsFromArguments(Transport.getDefaults(), arguments)) || this;
_this.name = "Transport";
//-------------------------------------
// LOOPING
//-------------------------------------
/**
* If the transport loops or not.
*/
_this._loop = new TimelineValue(false);
/**
* The loop start position in ticks
*/
_this._loopStart = 0;
/**
* The loop end position in ticks
*/
_this._loopEnd = 0;
//-------------------------------------
// TIMELINE EVENTS
//-------------------------------------
/**
* All the events in an object to keep track by ID
*/
_this._scheduledEvents = {};
/**
* The scheduled events.
*/
_this._timeline = new Timeline();
/**
* Repeated events
*/
_this._repeatedEvents = new IntervalTimeline();
/**
* All of the synced Signals
*/
_this._syncedSignals = [];
/**
* The swing amount
*/
_this._swingAmount = 0;
var options = optionsFromArguments(Transport.getDefaults(), arguments);
// CLOCK/TEMPO
_this._ppq = options.ppq;
_this._clock = new Clock({
callback: _this._processTick.bind(_this),
context: _this.context,
frequency: 0,
units: "bpm",
});
_this._bindClockEvents();
_this.bpm = _this._clock.frequency;
_this._clock.frequency.multiplier = options.ppq;
_this.bpm.setValueAtTime(options.bpm, 0);
readOnly(_this, "bpm");
_this._timeSignature = options.timeSignature;
// SWING
_this._swingTicks = options.ppq / 2; // 8n
return _this;
}
Transport.getDefaults = function () {
return Object.assign(ToneWithContext.getDefaults(), {
bpm: 120,
loopEnd: "4m",
loopStart: 0,
ppq: 192,
swing: 0,
swingSubdivision: "8n",
timeSignature: 4,
});
};
//-------------------------------------
// TICKS
//-------------------------------------
/**
* called on every tick
* @param tickTime clock relative tick time
*/
Transport.prototype._processTick = function (tickTime, ticks) {
// handle swing
if (this._swingAmount > 0 &&
ticks % this._ppq !== 0 && // not on a downbeat
ticks % (this._swingTicks * 2) !== 0) {
// add some swing
var progress = (ticks % (this._swingTicks * 2)) / (this._swingTicks * 2);
var 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.get(tickTime)) {
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, function (event) { return 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
* import { Transport } from "tone";
* // schedule an event on the 16th measure
* Transport.schedule((time) => {
* // invoked on measure 16
* console.log("measure 16!");
* }, "16:0:0");
*/
Transport.prototype.schedule = function (callback, time) {
var event = new TransportEvent(this, {
callback: 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
* import { Oscillator, Transport } from "tone";
* const osc = new Oscillator().toDestination().start();
* // a callback invoked every eighth note after the first measure
* Transport.scheduleRepeat((time) => {
* osc.start(time).stop(time + 0.1);
* }, "8n", "1m");
*/
Transport.prototype.scheduleRepeat = function (callback, interval, startTime, duration) {
if (duration === void 0) { duration = Infinity; }
var event = new TransportRepeatEvent(this, {
callback: 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.
* @param callback The callback to invoke once.
* @param time The time the callback should be invoked.
* @returns The ID of the scheduled event.
*/
Transport.prototype.scheduleOnce = function (callback, time) {
var event = new TransportEvent(this, {
callback: 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.
*/
Transport.prototype.clear = function (eventId) {
if (this._scheduledEvents.hasOwnProperty(eventId)) {
var 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
*/
Transport.prototype._addEvent = function (event, timeline) {
this._scheduledEvents[event.id.toString()] = {
event: event,
timeline: 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.
*/
Transport.prototype.cancel = function (after) {
var _this = this;
if (after === void 0) { after = 0; }
var computedAfter = this.toTicks(after);
this._timeline.forEachFrom(computedAfter, function (event) { return _this.clear(event.id); });
this._repeatedEvents.forEachFrom(computedAfter, function (event) { return _this.clear(event.id); });
return this;
};
//-------------------------------------
// START/STOP/PAUSE
//-------------------------------------
/**
* Bind start/stop/pause events from the clock and emit them.
*/
Transport.prototype._bindClockEvents = function () {
var _this = this;
this._clock.on("start", function (time, offset) {
offset = new TicksClass(_this.context, offset).toSeconds();
_this.emit("start", time, offset);
});
this._clock.on("stop", function (time) {
_this.emit("stop", time);
});
this._clock.on("pause", function (time) {
_this.emit("pause", time);
});
};
Object.defineProperty(Transport.prototype, "state", {
/**
* Returns the playback state of the source, either "started", "stopped", or "paused"
*/
get: function () {
return this._clock.getStateAtTime(this.now());
},
enumerable: true,
configurable: true
});
/**
* 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
* import { Transport } from "tone";
* // start the transport in one second starting at beginning of the 5th measure.
* Transport.start("+1", "4:0:0");
*/
Transport.prototype.start = function (time, offset) {
var 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
* import { Transport } from "tone";
* Transport.stop();
*/
Transport.prototype.stop = function (time) {
this._clock.stop(time);
return this;
};
/**
* Pause the transport and all sources synced to the transport.
*/
Transport.prototype.pause = function (time) {
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
*/
Transport.prototype.toggle = function (time) {
time = this.toSeconds(time);
if (this._clock.getStateAtTime(time) !== "started") {
this.start(time);
}
else {
this.stop(time);
}
return this;
};
Object.defineProperty(Transport.prototype, "timeSignature", {
//-------------------------------------
// 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
* import { Transport } from "tone";
* // 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: function () {
return this._timeSignature;
},
set: function (timeSig) {
if (isArray(timeSig)) {
timeSig = (timeSig[0] / timeSig[1]) * 4;
}
this._timeSignature = timeSig;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "loopStart", {
/**
* When the Transport.loop = true, this is the starting position of the loop.
*/
get: function () {
return new TimeClass(this.context, this._loopStart, "i").toSeconds();
},
set: function (startPosition) {
this._loopStart = this.toTicks(startPosition);
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "loopEnd", {
/**
* When the Transport.loop = true, this is the ending position of the loop.
*/
get: function () {
return new TimeClass(this.context, this._loopEnd, "i").toSeconds();
},
set: function (endPosition) {
this._loopEnd = this.toTicks(endPosition);
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "loop", {
/**
* If the transport loops or not.
*/
get: function () {
return this._loop.get(this.now());
},
set: function (loop) {
this._loop.set(loop, this.now());
},
enumerable: true,
configurable: true
});
/**
* Set the loop start and stop at the same time.
* @example
* import { Transport } from "tone";
* // loop over the first measure
* Transport.setLoopPoints(0, "1m");
* Transport.loop = true;
*/
Transport.prototype.setLoopPoints = function (startPosition, endPosition) {
this.loopStart = startPosition;
this.loopEnd = endPosition;
return this;
};
Object.defineProperty(Transport.prototype, "swing", {
/**
* The swing value. Between 0-1 where 1 equal to the note + half the subdivision.
*/
get: function () {
return this._swingAmount;
},
set: function (amount) {
// scale the values to a normal range
this._swingAmount = amount;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "swingSubdivision", {
/**
* 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: function () {
return new TicksClass(this.context, this._swingTicks).toNotation();
},
set: function (subdivision) {
this._swingTicks = this.toTicks(subdivision);
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "position", {
/**
* The Transport's position in Bars:Beats:Sixteenths.
* Setting the value will jump to that position right away.
*/
get: function () {
var now = this.now();
var ticks = this._clock.getTicksAtTime(now);
return new TicksClass(this.context, ticks).toBarsBeatsSixteenths();
},
set: function (progress) {
var ticks = this.toTicks(progress);
this.ticks = ticks;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "seconds", {
/**
* The Transport's position in seconds
* Setting the value will jump to that position right away.
*/
get: function () {
return this._clock.seconds;
},
set: function (s) {
var now = this.now();
var ticks = this._clock.frequency.timeToTicks(s, now);
this.ticks = ticks;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "progress", {
/**
* The Transport's loop position as a normalized value. Always
* returns 0 if the transport if loop is not true.
*/
get: function () {
if (this.loop) {
var now = this.now();
var ticks = this._clock.getTicksAtTime(now);
return (ticks - this._loopStart) / (this._loopEnd - this._loopStart);
}
else {
return 0;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty(Transport.prototype, "ticks", {
/**
* The transports current tick position.
*/
get: function () {
return this._clock.ticks;
},
set: function (t) {
if (this._clock.ticks !== t) {
var now = this.now();
// stop everything synced to the transport
if (this.state === "started") {
var ticks = this._clock.getTicksAtTime(now);
// schedule to start on the next tick, #573
var time = this._clock.getTimeOfTick(Math.ceil(ticks));
this.emit("stop", time);
this._clock.setTicksAtTime(t, time);
// restart it with the new time
this.emit("start", time, this._clock.getSecondsAtTime(time));
}
else {
this._clock.setTicksAtTime(t, now);
}
}
},
enumerable: true,
configurable: true
});
/**
* 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.
*/
Transport.prototype.getTicksAtTime = function (time) {
return Math.round(this._clock.getTicksAtTime(time));
};
/**
* Return the elapsed seconds at the given time.
* @param time When to get the elapsed seconds
* @return The number of elapsed seconds
*/
Transport.prototype.getSecondsAtTime = function (time) {
return this._clock.getSecondsAtTime(time);
};
Object.defineProperty(Transport.prototype, "PPQ", {
/**
* 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: function () {
return this._clock.frequency.multiplier;
},
set: function (ppq) {
this._clock.frequency.multiplier = ppq;
},
enumerable: true,
configurable: true
});
//-------------------------------------
// 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
* import { Transport } from "tone";
* // the transport must be started, otherwise returns 0
* Transport.start();
* Transport.nextSubdivision("4n");
*/
Transport.prototype.nextSubdivision = function (subdivision) {
subdivision = this.toTicks(subdivision);
if (this.state !== "started") {
// if the transport's not started, return 0
return 0;
}
else {
var now = this.now();
// the remainder of the current ticks and the subdivision
var transportPos = this.getTicksAtTime(now);
var 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.
*
* @param signal
* @param ratio Optionally pass in the ratio between the two signals.
* Otherwise it will be computed based on their current values.
*/
Transport.prototype.syncSignal = function (signal, ratio) {
if (!ratio) {
// get the sync ratio
var now = this.now();
if (signal.getValueAtTime(now) !== 0) {
var bpm = this.bpm.getValueAtTime(now);
var computedFreq = 1 / (60 / bpm / this.PPQ);
ratio = signal.getValueAtTime(now) / computedFreq;
}
else {
ratio = 0;
}
}
var ratioSignal = new Gain(ratio);
// @ts-ignore
this.bpm.connect(ratioSignal);
// @ts-ignore
ratioSignal.connect(signal._param);
this._syncedSignals.push({
initial: signal.value,
ratio: ratioSignal,
signal: signal,
});
signal.value = 0;
return this;
};
/**
* Unsyncs a previously synced signal from the transport's control.
* See Transport.syncSignal.
*/
Transport.prototype.unsyncSignal = function (signal) {
for (var i = this._syncedSignals.length - 1; i >= 0; i--) {
var syncedSignal = this._syncedSignals[i];
if (syncedSignal.signal === signal) {
syncedSignal.ratio.dispose();
syncedSignal.signal.value = syncedSignal.initial;
this._syncedSignals.splice(i, 1);
}
}
return this;
};
/**
* Clean up.
*/
Transport.prototype.dispose = function () {
_super.prototype.dispose.call(this);
this._clock.dispose();
writable(this, "bpm");
this._timeline.dispose();
this._repeatedEvents.dispose();
return this;
};
return Transport;
}(ToneWithContext));
export { Transport };
Emitter.mixin(Transport);
//-------------------------------------
// INITIALIZATION
//-------------------------------------
onContextInit(function (context) {
context.transport = new Transport({ context: context });
});
onContextClose(function (context) {
context.transport.dispose();
});
//# sourceMappingURL=Transport.js.map