diff --git a/Tone/core/Param.js b/Tone/core/Param.js index 763b3ba2..b0866b16 100644 --- a/Tone/core/Param.js +++ b/Tone/core/Param.js @@ -1,4 +1,4 @@ -define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/AudioParam"], function(Tone){ +define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/core/Timeline"], function(Tone){ "use strict"; @@ -45,6 +45,13 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au */ this.overridden = false; + /** + * The timeline which tracks all of the automations. + * @type {Tone.Timeline} + * @private + */ + this._events = new Tone.Timeline(1000); + if (!Tone.isUndef(options.value)){ this.value = options.value; } @@ -71,12 +78,12 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au */ Object.defineProperty(Tone.Param.prototype, "value", { get : function(){ - return this._toUnits(this._param.value); + var now = this.now(); + return this._toUnits(this.getValueAtTime(now)); }, set : function(value){ - var convertedVal = this._fromUnits(value); - this._param.cancelScheduledValues(0); - this._param.value = convertedVal; + this._initialValue = this._fromUnits(value); + this.setValueAtTime(value, this.now()); } }); @@ -175,6 +182,18 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au */ Tone.Param.prototype._minOutput = 0.00001; + /** + * The event types + * @enum {String} + * @private + */ + Tone.Param.AutomationType = { + Linear : "linearRampToValueAtTime", + Exponential : "exponentialRampToValueAtTime", + Target : "setTargetAtTime", + SetValue : "setValueAtTime" + }; + /** * Schedules a parameter value change at the given time. * @param {*} value The value to set the signal. @@ -186,8 +205,14 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au */ Tone.Param.prototype.setValueAtTime = function(value, time){ time = this.toSeconds(time); + value = this._fromUnits(value); Tone.isPast(time); - this._param.setValueAtTime(this._fromUnits(value), time); + this._events.add({ + "type" : Tone.Param.AutomationType.SetValue, + "value" : value, + "time" : time, + }); + this._param.setValueAtTime(value, time); return this; }; @@ -199,7 +224,32 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au */ Tone.Param.prototype.getValueAtTime = function(time){ time = this.toSeconds(time); - return this._fromUnits(this._param.getValueAtTime(time)); + var after = this._events.getAfter(time); + var before = this._events.get(time); + var initialValue = Tone.defaultArg(this._initialValue, this._param.defaultValue); + var value = initialValue; + //if it was set by + if (before === null){ + value = initialValue; + } else if (before.type === Tone.Param.AutomationType.Target){ + var previous = this._events.getBefore(before.time); + var previousVal; + if (previous === null){ + previousVal = initialValue; + } else { + previousVal = previous.value; + } + value = this._exponentialApproach(before.time, previousVal, before.value, before.constant, time); + } else if (after === null){ + value = before.value; + } else if (after.type === Tone.Param.AutomationType.Linear){ + value = this._linearInterpolate(before.time, before.value, after.time, after.value, time); + } else if (after.type === Tone.Param.AutomationType.Exponential){ + value = this._exponentialInterpolate(before.time, before.value, after.time, after.value, time); + } else { + value = before.value; + } + return value; }; /** @@ -217,7 +267,7 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au if (currentVal === 0){ currentVal = this._minOutput; } - this._param.setValueAtTime(currentVal, time); + this.setValueAtTime(this._toUnits(currentVal), time); return this; }; @@ -233,6 +283,11 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au value = this._fromUnits(value); endTime = this.toSeconds(endTime); Tone.isPast(endTime); + this._events.add({ + "type" : Tone.Param.AutomationType.Linear, + "value" : value, + "time" : endTime, + }); this._param.linearRampToValueAtTime(value, endTime); return this; }; @@ -250,6 +305,12 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au value = Math.max(this._minOutput, value); endTime = this.toSeconds(endTime); Tone.isPast(endTime); + //store the event + this._events.add({ + "type" : Tone.Param.AutomationType.Exponential, + "time" : endTime, + "value" : value + }); this._param.exponentialRampToValueAtTime(value, endTime); return this; }; @@ -351,7 +412,14 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au if (timeConstant <= 0){ throw new Error("timeConstant must be greater than 0"); } - this._param.setTargetAtTime(value, this.toSeconds(startTime), timeConstant); + startTime = this.toSeconds(startTime) + this._events.add({ + "type" : Tone.Param.AutomationType.Target, + "value" : value, + "time" : startTime, + "constant" : timeConstant + }); + this._param.setTargetAtTime(value, startTime, timeConstant); return this; }; @@ -372,7 +440,7 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au this.setValueAtTime(values[0] * scaling, startTime); var segTime = duration / (values.length - 1); for (var i = 1; i < values.length; i++){ - this._param.linearRampToValueAtTime(this._fromUnits(values[i] * scaling), startTime + i * segTime); + this.linearRampToValueAtTime(values[i] * scaling, startTime + i * segTime); } return this; }; @@ -381,22 +449,75 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au * Cancels all scheduled parameter changes with times greater than or * equal to startTime. * - * @param {Time} startTime + * @param {Time} time * @returns {Tone.Param} this */ - Tone.Param.prototype.cancelScheduledValues = function(startTime){ - this._param.cancelScheduledValues(this.toSeconds(startTime)); + Tone.Param.prototype.cancelScheduledValues = function(time){ + time = this.toSeconds(time); + this._events.cancel(time); + this._param.cancelScheduledValues(time); return this; }; /** * This is similar to [cancelScheduledValues](#cancelScheduledValues) except - * it holds the automated value at cancelTime until the next automated event. - * @param {Time} cancelTime + * it holds the automated value at time until the next automated event. + * @param {Time} time * @returns {Tone.Param} this */ - Tone.Param.prototype.cancelAndHoldAtTime = function(cancelTime){ - this._param.cancelAndHoldAtTime(this.toSeconds(cancelTime)); + Tone.Param.prototype.cancelAndHoldAtTime = function(time){ + var valueAtTime = this.getValueAtTime(time); + //if there is an event at the given time + //and that even is not a "set" + var before = this._events.get(time); + var after = this._events.getAfter(time); + if (before && before.time === time){ + //remove everything after + if (after){ + this._events.cancel(after.time); + } else { + this._events.cancel(time + 1e-6); + } + } else if (after){ + //cancel the next event(s) + this._events.cancel(after.time); + if (!this._param.cancelAndHoldAtTime){ + this._param.cancelScheduledValues(time); + } + if (after.type === Tone.Param.AutomationType.Linear){ + if (!this._param.cancelAndHoldAtTime){ + this.linearRampToValueAtTime(valueAtTime, time); + } else { + this._events.add({ + "type" : Tone.Param.AutomationType.Linear, + "value" : valueAtTime, + "time" : time + }); + } + } else if (after.type === Tone.Param.AutomationType.Exponential){ + if (!this._param.cancelAndHoldAtTime){ + this.exponentialRampToValueAtTime(valueAtTime, time); + } else { + this._events.add({ + "type" : Tone.Param.AutomationType.Exponential, + "value" : valueAtTime, + "time" : time + }); + } + } + } + + //set the value at the given time + this._events.add({ + "type" : Tone.Param.AutomationType.SetValue, + "value" : valueAtTime, + "time" : time + }); + if (this._param.cancelAndHoldAtTime){ + return this._param.cancelAndHoldAtTime(time); + } else { + return this._param.setValueAtTime(valueAtTime, time); + } return this; }; @@ -428,6 +549,26 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au return this; }; + /////////////////////////////////////////////////////////////////////////// + // AUTOMATION CURVE CALCULATIONS + // MIT License, copyright (c) 2014 Jordan Santell + /////////////////////////////////////////////////////////////////////////// + + // Calculates the the value along the curve produced by setTargetAtTime + Tone.Param.prototype._exponentialApproach = function (t0, v0, v1, timeConstant, t) { + return v1 + (v0 - v1) * Math.exp(-(t - t0) / timeConstant); + }; + + // Calculates the the value along the curve produced by linearRampToValueAtTime + Tone.Param.prototype._linearInterpolate = function (t0, v0, t1, v1, t) { + return v0 + (v1 - v0) * ((t - t0) / (t1 - t0)); + }; + + // Calculates the the value along the curve produced by exponentialRampToValueAtTime + Tone.Param.prototype._exponentialInterpolate = function (t0, v0, t1, v1, t) { + return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0)); + }; + /** * Clean up * @returns {Tone.Param} this @@ -435,6 +576,7 @@ define(["Tone/core/Tone", "Tone/type/Type", "Tone/core/AudioNode", "Tone/shim/Au Tone.Param.prototype.dispose = function(){ Tone.AudioNode.prototype.dispose.call(this); this._param = null; + this._events = null; return this; }; diff --git a/test/core/Param.js b/test/core/Param.js index 42866f90..9ccb003f 100644 --- a/test/core/Param.js +++ b/test/core/Param.js @@ -59,7 +59,7 @@ define(["helper/Basic", "Test", "Tone/core/Param", "Tone/type/Type", "Tone/signa "units" : Tone.Type.Decibels, }); param.value = -10; - expect(param._param.value).to.be.closeTo(0.315, 0.01); + expect(param.value).to.be.closeTo(-10, 0.01); param.dispose(); }); @@ -71,7 +71,7 @@ define(["helper/Basic", "Test", "Tone/core/Param", "Tone/type/Type", "Tone/signa "convert" : false }); param.value = -10; - expect(param._param.value).to.be.closeTo(-10, 0.001); + expect(param.value).to.be.closeTo(-10, 0.001); param.dispose(); });