diff --git a/Tone/signal/TransportTimelineSignal.js b/Tone/signal/TransportTimelineSignal.js deleted file mode 100644 index 712f92b93..000000000 --- a/Tone/signal/TransportTimelineSignal.js +++ /dev/null @@ -1,188 +0,0 @@ -import Tone from "../core/Tone"; -import "../core/Transport"; -import "../signal/Signal"; -import "../type/TransportTime"; - -/** - * @class Tone.TransportTimelineSignal extends Tone.Signal, but adds the ability to synchronize the signal to the signal to the Tone.Transport - * @extends {Tone.Signal} - */ -Tone.TransportTimelineSignal = function(){ - Tone.Signal.apply(this, arguments); - - /** - * The real signal output - * @type {Tone.Signal} - * @private - */ - this.output = this._outputSig = new Tone.Signal(this._initialValue); - - /** - * Keep track of the last value. (small optimization) - * @private - * @type {Number} - */ - this._lastVal = this.value; - - /** - * The event id of the tick update loop - * @private - * @type {Number} - */ - this._synced = Tone.Transport.scheduleRepeat(this._onTick.bind(this), "1i"); - - /** - * A bound version of the anchor value methods - * @type {Function} - * @private - */ - this._bindAnchorValue = this._anchorValue.bind(this); - Tone.Transport.on("start stop pause", this._bindAnchorValue); - - this._events.memory = Infinity; -}; - -Tone.extend(Tone.TransportTimelineSignal, Tone.Signal); - -/** - * Callback which is invoked every tick. - * @private - * @param {Number} time - * @return {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype._onTick = function(time){ - var val = this.getValueAtTime(Tone.Transport.seconds); - if (this._lastVal !== val){ - this._lastVal = val; - //approximate ramp curves with linear ramps - this._outputSig.linearRampToValueAtTime(val, time); - } -}; - -/** - * Anchor the value at the start and stop of the Transport - * @param {Number} time The time of the event - * @return {Tone.TransportTimelineSignal} this - * @private - */ -Tone.TransportTimelineSignal.prototype._anchorValue = function(time){ - var val = this.getValueAtTime(Tone.Transport.seconds); - this._lastVal = val; - this._outputSig.cancelScheduledValues(time); - this._outputSig.setValueAtTime(val, time); - return this; -}; - -/** - * Get the scheduled value at the given time. This will - * return the unconverted (raw) value. - * @param {TransportTime} time The time in seconds. - * @return {Number} The scheduled value at the given time. - */ -Tone.TransportTimelineSignal.prototype.getValueAtTime = function(time){ - time = Tone.TransportTime(time); - return Tone.Signal.prototype.getValueAtTime.call(this, time); -}; - -/** - * Set the output of the signal at the given time - * @param {Number} value The value to change to at the given time - * @param {TransportTime} time The time to change the signal - * @return {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype.setValueAtTime = function(value, time){ - time = Tone.TransportTime(time); - Tone.Signal.prototype.setValueAtTime.call(this, value, time); - return this; -}; - -/** - * Linear ramp to the given value from the previous scheduled point to the given value - * @param {Number} value The value to change to at the given time - * @param {TransportTime} time The time to change the signal - * @return {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype.linearRampToValueAtTime = function(value, time){ - time = Tone.TransportTime(time); - Tone.Signal.prototype.linearRampToValueAtTime.call(this, value, time); - return this; -}; - -/** - * Exponential ramp to the given value from the previous scheduled point to the given value - * @param {Number} value The value to change to at the given time - * @param {TransportTime} time The time to change the signal - * @return {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype.exponentialRampToValueAtTime = function(value, time){ - time = Tone.TransportTime(time); - Tone.Signal.prototype.exponentialRampToValueAtTime.call(this, value, time); - return this; -}; - -/** - * Start exponentially approaching the target value at the given time with - * a rate having the given time constant. - * @param {number} value - * @param {TransportTime} startTime - * @param {number} timeConstant - * @return {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype.setTargetAtTime = function(value, startTime, timeConstant){ - startTime = Tone.TransportTime(startTime); - Tone.Signal.prototype.setTargetAtTime.call(this, value, startTime, timeConstant); - return this; -}; - -/** - * Cancels all scheduled parameter changes with times greater than or - * equal to startTime. - * @param {TransportTime} startTime - * @returns {Tone.Param} this - */ -Tone.TransportTimelineSignal.prototype.cancelScheduledValues = function(startTime){ - startTime = Tone.TransportTime(startTime); - Tone.Signal.prototype.cancelScheduledValues.call(this, startTime); - return this; -}; - -/** - * Set an array of arbitrary values starting at the given time for the given duration. - * @param {Float32Array} values - * @param {Time} startTime - * @param {Time} duration - * @param {NormalRange} [scaling=1] If the values in the curve should be scaled by some value - * @returns {Tone.Signal} this - */ -Tone.TransportTimelineSignal.prototype.setValueCurveAtTime = function(values, startTime, duration, scaling){ - startTime = Tone.TransportTime(startTime); - duration = Tone.TransportTime(duration); - Tone.Signal.prototype.setValueCurveAtTime.call(this, values, startTime, duration, scaling); - return this; -}; - -/** - * This is similar to [cancelScheduledValues](#cancelScheduledValues) except - * it holds the automated value at time until the next automated event. - * @param {Time} time - * @returns {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype.cancelAndHoldAtTime = function(time){ - return Tone.Signal.prototype.cancelAndHoldAtTime.call(this, Tone.TransportTime(time)); -}; - -/** - * Dispose and disconnect - * @return {Tone.TransportTimelineSignal} this - */ -Tone.TransportTimelineSignal.prototype.dispose = function(){ - Tone.Transport.clear(this._synced); - Tone.Transport.off("start stop pause", this._syncedCallback); - this._events.cancel(0); - Tone.Signal.prototype.dispose.call(this); - this._outputSig.dispose(); - this._outputSig = null; -}; - -export default Tone.TransportTimelineSignal; - diff --git a/Tone/signal/TransportTimelineSignal.test.ts b/Tone/signal/TransportTimelineSignal.test.ts new file mode 100644 index 000000000..a39acd9aa --- /dev/null +++ b/Tone/signal/TransportTimelineSignal.test.ts @@ -0,0 +1,245 @@ +import { TransportTimelineSignal } from "./TransportTimelineSignal"; +import { Offline } from "test/helper/Offline"; +import { expect } from "chai"; +import { dbToGain } from "Tone/core/type/Conversions"; +import "../core/clock/Transport"; +import "../core/context/Destination"; +import { BasicTests } from "test/helper/Basic"; + +describe("TransportTimelineSignal", () => { + + BasicTests(TransportTimelineSignal); + + context("Scheduling Events", () => { + + it("can schedule a change in the future", () => { + const sched = new TransportTimelineSignal(1); + sched.setValueAtTime(2, 0.2); + sched.dispose(); + }); + + it("can schedule setValueAtTime relative to the Transport", () => { + return Offline(({ transport }) => { + const sched = new TransportTimelineSignal(1).toDestination(); + sched.setValueAtTime(2, 0.1); + sched.setValueAtTime(3, 0.2); + transport.start(0.1); + }, 0.4, 1).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.201)).to.be.closeTo(2, 0.07); + expect(buffer.getValueAtTime(0.301)).to.be.closeTo(3, 0.07); + }); + }); + + it("can schedule linearRampToValueAtTime relative to the Transport", () => { + return Offline(({ transport }) => { + const sched = new TransportTimelineSignal(1).toDestination(); + sched.setValueAtTime(1, 0.1); + sched.linearRampToValueAtTime(2, 0.2); + transport.start(0.1); + }, 0.4, 1).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.2)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.25)).to.be.closeTo(1.5, 0.07); + expect(buffer.getValueAtTime(0.301)).to.be.closeTo(2, 0.07); + }); + }); + + it("can schedule exponentialRampToValueAtTime relative to the Transport", () => { + return Offline(({ transport }) => { + const sched = new TransportTimelineSignal(1).toDestination(); + sched.setValueAtTime(1, 0.1); + sched.exponentialRampToValueAtTime(2, 0.2); + transport.start(0.1); + }, 0.4, 1).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.1)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.2)).to.be.closeTo(1, 0.07); + expect(buffer.getValueAtTime(0.25)).to.be.closeTo(1.4, 0.07); + expect(buffer.getValueAtTime(0.301)).to.be.closeTo(2, 0.07); + }); + }); + + it("can get exponential ramp value in the future", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(0.5).toDestination(); + sched.setValueAtTime(0.5, 0); + sched.exponentialRampToValueAtTime(1, 0.2); + sched.exponentialRampToValueAtTime(0.5, 0.4); + transport.start(0.1); + }, 0.6).then((buffer) => { + buffer.forEach((sample, time) => { + expect(sample).to.be.closeTo(sched.getValueAtTime(time - 0.1), 0.07); + }); + }); + }); + + it("can get exponential approach in the future", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(0.5).toDestination(); + sched.setValueAtTime(0.5, 0); + sched.setTargetAtTime(1, 0.2, 0.2); + transport.start(0.1); + }, 0.6).then((buffer) => { + buffer.forEach((sample, time) => { + expect(sample).to.be.closeTo(sched.getValueAtTime(time - 0.1), 0.07); + }); + }); + }); + + it("can loop the signal when the Transport loops", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(1).toDestination(); + transport.setLoopPoints(0, 1); + transport.loop = true; + sched.setValueAtTime(1, 0); + sched.setValueAtTime(2, 0.5); + transport.start(0); + }, 2).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.01); + expect(buffer.getValueAtTime(0.5)).to.be.closeTo(2, 0.01); + expect(buffer.getValueAtTime(0)).to.be.closeTo(1, 0.01); + expect(buffer.getValueAtTime(1.5)).to.be.closeTo(2, 0.01); + }); + }); + + it("can get set a curve in the future", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(0).toDestination(); + sched.setValueCurveAtTime([0, 1, 0.2, 0.8, 0], 0, 1); + transport.start(0.2); + }, 1).then((buffer) => { + buffer.forEach((sample, time) => { + expect(sample).to.be.closeTo(sched.getValueAtTime(time - 0.2), 0.07); + }); + }); + }); + + it("can scale a curve value", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(1).toDestination(); + sched.setValueCurveAtTime([0, 1, 0], 0, 1, 0.5); + transport.start(0); + }, 1).then((buffer) => { + buffer.forEach((sample) => { + expect(sample).to.be.at.most(0.51); + }); + }); + }); + + it("can schedule a linear ramp between two times", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(0).toDestination(); + sched.linearRampTo(1, 1, 1); + transport.start(0); + }, 3).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.closeTo(0, 0.1); + expect(buffer.getValueAtTime(0.5)).to.closeTo(0, 0.1); + expect(buffer.getValueAtTime(1)).to.closeTo(0, 0.1); + expect(buffer.getValueAtTime(1.5)).to.closeTo(0.5, 0.1); + expect(buffer.getValueAtTime(2)).to.closeTo(1, 0.1); + }); + }); + + it("can get exponential ramp value between two times", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(1).toDestination(); + sched.exponentialRampTo(3, 1, 1); + transport.start(0); + }, 3).then((buffer) => { + buffer.forEach((sample, time) => { + expect(sample).to.be.closeTo(sched.getValueAtTime(time), 0.02); + }); + }); + }); + + it("can cancel and hold a scheduled value", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(0).toDestination(); + sched.setValueAtTime(0, 0); + sched.linearRampToValueAtTime(1, 1); + sched.cancelAndHoldAtTime(0.5); + transport.start(0); + }, 1).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.1); + expect(buffer.getValueAtTime(0.25)).to.be.closeTo(0.25, 0.1); + expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0.5, 0.1); + expect(buffer.getValueAtTime(0.75)).to.be.closeTo(0.5, 0.1); + }); + }); + + it("can cancel a scheduled value", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(0).toDestination(); + sched.setValueAtTime(0, 0); + sched.linearRampToValueAtTime(1, 0.5); + sched.linearRampToValueAtTime(0, 1); + sched.cancelScheduledValues(0.6); + transport.start(0); + }, 1).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.1); + expect(buffer.getValueAtTime(0.25)).to.be.closeTo(0.5, 0.1); + expect(buffer.getValueAtTime(0.5)).to.be.closeTo(1, 0.1); + expect(buffer.getValueAtTime(0.75)).to.be.closeTo(1, 0.1); + }); + }); + + it("can automate values with different units", () => { + let sched; + return Offline(({ transport }) => { + sched = new TransportTimelineSignal(-10, "decibels").toDestination(); + sched.setValueAtTime(-5, 0); + sched.linearRampToValueAtTime(-12, 0.5); + sched.exponentialRampTo(-6, 0.1, 1); + transport.start(0); + }, 1.2).then((buffer) => { + buffer.forEach((sample, time) => { + if (time < 0.5) { + expect(sample).to.be.within(dbToGain(-12.01), dbToGain(-4.99)); + } else if (time < 1) { + expect(sample).to.be.closeTo(dbToGain(-12), 0.1); + } else if (time > 1.1) { + expect(sample).to.be.closeTo(dbToGain(-6), 0.1); + } + }); + }); + }); + + it("can set a ramp point and then ramp from there", async () => { + const buffer = await Offline(({ transport }) => { + const sig = new TransportTimelineSignal(0).toDestination(); + sig.setRampPoint(0); + sig.linearRampToValueAtTime(1, 1); + sig.setRampPoint(0.5); + sig.linearRampToValueAtTime(0, 1); + transport.start(0); + }, 1); + expect(buffer.getValueAtTime(0)).to.be.closeTo(0, 0.1); + expect(buffer.getValueAtTime(0.5)).to.be.closeTo(0.5, 0.1); + expect(buffer.getValueAtTime(1)).to.be.closeTo(0, 0.1); + }); + + it("can set a exponential approach ramp from the current time", () => { + return Offline(({ transport }) => { + const sig = new TransportTimelineSignal(0).toDestination(); + sig.targetRampTo(1, 0.3); + transport.start(0); + }, 0.5).then((buffer) => { + expect(buffer.getValueAtTime(0)).to.be.below(0.01); + expect(buffer.getValueAtTime(0.3)).to.be.closeTo(1, 0.1); + }); + }); + }); +}); + diff --git a/Tone/signal/TransportTimelineSignal.ts b/Tone/signal/TransportTimelineSignal.ts new file mode 100644 index 000000000..b51eaf778 --- /dev/null +++ b/Tone/signal/TransportTimelineSignal.ts @@ -0,0 +1,172 @@ +import { Signal, SignalOptions } from "./Signal"; +import { NormalRange, Seconds, Time, TransportTime, UnitMap, UnitName } from "../core/type/Units"; +import { optionsFromArguments } from "../core/util/Defaults"; +import { TransportTimeClass } from "../core/type/TransportTime"; +import { ToneConstantSource } from "./ToneConstantSource"; +import { OutputNode } from "../core/context/ToneAudioNode"; + +/** + * Adds the ability to synchronize the signal to the [[Transport]] + */ +export class TransportTimelineSignal extends Signal { + + readonly name: string = "TransportTimelineSignal"; + + /** + * Don't override when something is connected to the input + */ + readonly override = false; + + readonly output: OutputNode; + + /** + * Keep track of the last value as an optimization. + */ + private _lastVal: UnitMap[TypeName]; + + /** + * The ID returned from scheduleRepeat + */ + private _synced: number; + + /** + * Remember the callback value + */ + private _syncedCallback: () => void; + + /** + * @param value Initial value of the signal + * @param units The unit name, e.g. "frequency" + */ + constructor(value?: UnitMap[TypeName], units?: TypeName); + constructor(options?: Partial>); + constructor() { + + super(optionsFromArguments(Signal.getDefaults(), arguments, ["value", "units"])); + const options = optionsFromArguments(Signal.getDefaults(), arguments, ["value", "units"]) as SignalOptions; + + this._lastVal = options.value; + this._synced = this.context.transport.scheduleRepeat(this._onTick.bind(this), "1i"); + + this._syncedCallback = this._anchorValue.bind(this); + this.context.transport.on("start", this._syncedCallback); + this.context.transport.on("pause", this._syncedCallback); + this.context.transport.on("stop", this._syncedCallback); + + // disconnect the constant source from the output and replace it with another one + this._constantSource.disconnect(); + this._constantSource.stop(0); + + // create a new one + this._constantSource = this.output = new ToneConstantSource({ + context: this.context, + offset: options.value, + units: options.units, + }).start(0); + this.setValueAtTime(options.value, 0); + } + + /** + * Callback which is invoked every tick. + */ + private _onTick(time: Seconds): void { + const val = super.getValueAtTime(this.context.transport.seconds); + // approximate ramp curves with linear ramps + if (this._lastVal !== val) { + this._lastVal = val; + this._constantSource.offset.setValueAtTime(val, time); + } + } + + /** + * Anchor the value at the start and stop of the Transport + */ + private _anchorValue(time: Seconds): void { + const val = super.getValueAtTime(this.context.transport.seconds); + this._lastVal = val; + this._constantSource.offset.cancelAndHoldAtTime(time); + this._constantSource.offset.setValueAtTime(val, time); + } + + getValueAtTime(time: TransportTime): UnitMap[TypeName] { + const computedTime = new TransportTimeClass(this.context, time).toSeconds(); + return super.getValueAtTime(computedTime); + } + + setValueAtTime(value: UnitMap[TypeName], time: TransportTime) { + const computedTime = new TransportTimeClass(this.context, time).toSeconds(); + super.setValueAtTime(value, computedTime); + return this; + } + + linearRampToValueAtTime(value: UnitMap[TypeName], time: TransportTime) { + const computedTime = new TransportTimeClass(this.context, time).toSeconds(); + super.linearRampToValueAtTime(value, computedTime); + return this; + } + + exponentialRampToValueAtTime(value: UnitMap[TypeName], time: TransportTime) { + const computedTime = new TransportTimeClass(this.context, time).toSeconds(); + super.exponentialRampToValueAtTime(value, computedTime); + return this; + } + + setTargetAtTime(value, startTime: TransportTime, timeConstant: number): this { + const computedTime = new TransportTimeClass(this.context, startTime).toSeconds(); + super.setTargetAtTime(value, computedTime, timeConstant); + return this; + } + + cancelScheduledValues(startTime: TransportTime): this { + const computedTime = new TransportTimeClass(this.context, startTime).toSeconds(); + super.cancelScheduledValues(computedTime); + return this; + } + + setValueCurveAtTime(values: UnitMap[TypeName][], startTime: TransportTime, duration: Time, scaling: NormalRange): this { + const computedTime = new TransportTimeClass(this.context, startTime).toSeconds(); + duration = this.toSeconds(duration); + super.setValueCurveAtTime(values, computedTime, duration, scaling); + return this; + } + + cancelAndHoldAtTime(time: TransportTime): this { + const computedTime = new TransportTimeClass(this.context, time).toSeconds(); + super.cancelAndHoldAtTime(computedTime); + return this; + } + + setRampPoint(time: TransportTime): this { + const computedTime = new TransportTimeClass(this.context, time).toSeconds(); + super.setRampPoint(computedTime); + return this; + } + + exponentialRampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: TransportTime): this { + const computedTime = new TransportTimeClass(this.context, startTime).toSeconds(); + super.exponentialRampTo(value, rampTime, computedTime); + return this; + } + + linearRampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: TransportTime): this { + const computedTime = new TransportTimeClass(this.context, startTime).toSeconds(); + super.linearRampTo(value, rampTime, computedTime); + return this; + } + + targetRampTo(value: UnitMap[TypeName], rampTime: Time, startTime?: TransportTime): this { + const computedTime = new TransportTimeClass(this.context, startTime).toSeconds(); + super.targetRampTo(value, rampTime, computedTime); + return this; + } + + dispose(): this { + super.dispose(); + this.context.transport.clear(this._synced); + this.context.transport.off("start", this._syncedCallback); + this.context.transport.off("pause", this._syncedCallback); + this.context.transport.off("stop", this._syncedCallback); + this._constantSource.dispose(); + return this; + } +}