Commit 4260025d authored by rtoy's avatar rtoy Committed by Commit bot

Implement cancelValuesAndHoldAtTime

cancelValuesAndHoldAtTime(time) should cancel all events after time |time|
and when the context current time reaches |time|, the automation value
that is current at that time is held as if setValueAtTime were called
with the appropriate value and time.

Spec: https://webaudio.github.io/web-audio-api/#widl-AudioParam-cancelAndHoldAtTime-AudioParam-double-cancelTime
Intent: https://groups.google.com/a/chromium.org/d/msg/blink-dev/IftXib6yFyw/CLoxngG9BgAJ

BUG=432934
TEST=audioparam-cancel-scheduled-values.html

Review-Url: https://codereview.chromium.org/1533103002
Cr-Commit-Position: refs/heads/master@{#442916}
parent 97501194
...@@ -149,6 +149,7 @@ interface AudioParam ...@@ -149,6 +149,7 @@ interface AudioParam
getter minValue getter minValue
getter value getter value
method cancelScheduledValues method cancelScheduledValues
method cancelValuesAndHoldAtTime
method constructor method constructor
method exponentialRampToValueAtTime method exponentialRampToValueAtTime
method linearRampToValueAtTime method linearRampToValueAtTime
......
...@@ -149,6 +149,7 @@ interface AudioParam ...@@ -149,6 +149,7 @@ interface AudioParam
getter minValue getter minValue
getter value getter value
method cancelScheduledValues method cancelScheduledValues
method cancelValuesAndHoldAtTime
method constructor method constructor
method exponentialRampToValueAtTime method exponentialRampToValueAtTime
method linearRampToValueAtTime method linearRampToValueAtTime
......
...@@ -149,6 +149,7 @@ interface AudioParam ...@@ -149,6 +149,7 @@ interface AudioParam
getter minValue getter minValue
getter value getter value
method cancelScheduledValues method cancelScheduledValues
method cancelValuesAndHoldAtTime
method constructor method constructor
method exponentialRampToValueAtTime method exponentialRampToValueAtTime
method linearRampToValueAtTime method linearRampToValueAtTime
......
<!doctype html>
<html>
<head>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/audit-util.js"></script>
<script src="../resources/audit.js"></script>
<title>Test CancelValuesAndHoldAtTime</title>
</head>
<body>
<script>
let sampleRate = 48000;
let renderDuration = 0.5;
let audit = Audit.createTaskRunner();
// The first few tasks test the cancellation of each relevant automation
// function. For the test, a simple linear ramp from 0 to 1 is used to
// start things off. Then the automation to be tested is scheduled and
// cancelled.
audit.define("linear", function (task, should) {
task.describe("Cancel linearRampToValueAtTime");
cancelTest(should, linearRampTest("linearRampToValueAtTime"), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}).then(task.done.bind(task));
});
audit.define("exponential", function (task, should) {
task.describe("Cancel exponentialRampAtTime");
// Cancel an exponential ramp. The thresholds are experimentally
// determined.
cancelTest(should, function (g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// After the linear ramp, schedule an exponential ramp to the end.
// (This is the event that will be be cancelled.)
let v1 = 0.001;
let t1 = renderDuration;
g[0].gain.exponentialRampToValueAtTime(v1, t1);
g[1].gain.exponentialRampToValueAtTime(v1, t1);
expectedConstant = Math.fround(v0 * Math.pow(v1 / v0, (
cancelTime -
t0) / (t1 - t0)));
return {
expectedConstant: expectedConstant,
autoMessage: "exponentialRampToValue(" + v1 + ", " + t1 + ")",
summary: "exponentialRampToValueAtTime",
};
}, {
valueThreshold: 1.8664e-6,
curveThreshold: 5.9605e-8
}).then(task.done.bind(task));
});
audit.define("setTarget", function (task, should) {
task.describe("Cancel setTargetAtTime");
// Cancel a setTarget event.
cancelTest(should, function (g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// At the end of the linear ramp, schedule a setTarget. (This is the
// event that will be cancelled.)
let v1 = 0;
let t1 = t0;
let timeConstant = 0.05;
g[0].gain.setTargetAtTime(v1, t1, timeConstant);
g[1].gain.setTargetAtTime(v1, t1, timeConstant);
expectedConstant = Math.fround(v1 + (v0 - v1) * Math.exp(-(
cancelTime - t0) / timeConstant));
return {
expectedConstant: expectedConstant,
autoMessage: "setTargetAtTime(" + v1 + ", " + t1 + ", " +
timeConstant + ")",
summary: "setTargetAtTime",
};
}, {
valueThreshold: 4.5267e-7, //1.1317e-7,
curveThreshold: 0
}).then(task.done.bind(task));
});
audit.define("setValueCurve", function (task, should) {
task.describe("Cancel setValueCurveAtTime");
// Cancel a setValueCurve event.
cancelTest(should, function (g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// After the linear ramp, schedule a setValuesCurve. (This is the
// event that will be cancelled.)
let v1 = 0;
let duration = renderDuration - t0;
// For simplicity, a 2-point curve so we get a linear interpolated result.
let curve = Float32Array.from([v0, 0]);
g[0].gain.setValueCurveAtTime(curve, t0, duration);
g[1].gain.setValueCurveAtTime(curve, t0, duration);
let index = Math.floor((curve.length - 1) / duration * (
cancelTime - t0));
let curvePointsPerFrame = (curve.length - 1) / duration /
sampleRate;
let virtualIndex = (cancelTime - t0) * sampleRate *
curvePointsPerFrame;
let delta = virtualIndex - index;
expectedConstant = curve[0] + (curve[1] - curve[0]) * delta;
return {
expectedConstant: expectedConstant,
autoMessage: "setValueCurveAtTime([" + curve + "], " + t0 +
", " + duration +
")",
summary: "setValueCurveAtTime",
};
}, {
valueThreshold: 9.5368e-9,
curveThreshold: 0
}).then(task.done.bind(task));
});
audit.define("setValueCurve after end", function (task, should) {
task.describe("Cancel setValueCurveAtTime after the end");
cancelTest(should, function (g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// After the linear ramp, schedule a setValuesCurve. (This is the
// event that will be cancelled.) Make sure the curve ends before the
// cancellation time.
let v1 = 0;
let duration = cancelTime - t0 - 0.125;
// For simplicity, a 2-point curve so we get a linear interpolated
// result.
let curve = Float32Array.from([v0, 0]);
g[0].gain.setValueCurveAtTime(curve, t0, duration);
g[1].gain.setValueCurveAtTime(curve, t0, duration);
expectedConstant = curve[1];
return {
expectedConstant: expectedConstant,
autoMessage: "setValueCurveAtTime([" + curve + "], " + t0 +
", " + duration +
")",
summary: "setValueCurveAtTime",
};
}, {
valueThreshold: 0,
curveThreshold: 0
}).then(task.done.bind(task));
});
// Special case where we schedule a setTarget and there is no earlier
// automation event. This tests that we pick up the starting point
// correctly from the last setting of the AudioParam value attribute.
audit.define("initial setTarget", function (task, should) {
task.describe("Cancel with initial setTargetAtTime");
cancelTest(should, function (g, v0, t0, cancelTime) {
let v1 = 0;
let timeConstant = 0.1;
g[0].gain.value = 1;
g[0].gain.setTargetAtTime(v1, t0, timeConstant);
g[1].gain.value = 1;
g[1].gain.setTargetAtTime(v1, t0, timeConstant);
let expectedConstant = Math.fround(v1 + (v0 - v1) * Math.exp(-
(cancelTime - t0) /
timeConstant));
return {
expectedConstant: expectedConstant,
autoMessage: "setTargetAtTime(" + v1 + ", " + t0 + ", " +
timeConstant + ")",
summary: "Initial setTargetAtTime",
};
}, {
valueThreshold: 1.2320e-6,
curveThreshold: 0
}).then(task.done.bind(task));
});
// Test automations scheduled after the call to cancelValuesAndHoldAtTime.
// Very similar to the above tests, but we also schedule an event after
// cancelValuesAndHoldAtTime and verify that curve after cancellation has
// the correct values.
audit.define("post cancel: Linear", function (task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule another linear ramp after the cancellation.
task.describe("LinearRamp after cancelling");
cancelTest(should, linearRampTest(
"Post cancellation linearRampToValueAtTime"), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}, function (g, cancelTime, expectedConstant) {
// Schedule the linear ramp on g[0], and do the same for g[2], using the starting point
// given by expectedConstant.
let v2 = 2;
let t2 = cancelTime + 0.125;
g[0].gain.linearRampToValueAtTime(v2, t2);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.linearRampToValueAtTime(v2, t2);
return {
constantEndTime: cancelTime,
message: "Post linearRamp(" + v2 + ", " + t2 + ")"
};
}).then(task.done.bind(task));
});
audit.define("post cancel: Exponential", function (task, should) {
task.describe("ExponentialRamp after cancelling");
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule an exponential ramp after the cancellation.
cancelTest(should, linearRampTest(
"Post cancel exponentialRampToValueAtTime"), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}, function (g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for g[2],
// using the starting point given by expectedConstant.
let v2 = 2;
let t2 = cancelTime + 0.125;
g[0].gain.exponentialRampToValueAtTime(v2, t2);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.exponentialRampToValueAtTime(v2, t2);
return {
constantEndTime: cancelTime,
message: "Post exponentialRamp(" + v2 + ", " + t2 + ")"
};
}).then(task.done.bind(task));
});
audit.define("post cancel: ValueCurve", function (task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule a setValueCurve after the cancellation.
cancelTest(should, linearRampTest("Post cancel setValueCurveAtTime"), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}, function (g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for g[2],
// using the starting point given by expectedConstant.
let t2 = cancelTime + 0.125;
let duration = 0.125;
let curve = Float32Array.from([.125, 2]);
g[0].gain.setValueCurveAtTime(curve, t2, duration);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.setValueCurveAtTime(curve, t2, duration);
return {
constantEndTime: cancelTime,
message: "Post setValueCurve([" + curve + "], " + t2 + ", " +
duration + ")",
errorThreshold: 8.3998e-5
};
}).then(task.done.bind(task));
});
audit.define("post cancel: setTarget", function (task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule a setTarget after the cancellation.
cancelTest(should, linearRampTest("Post cancel setTargetAtTime"), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}, function (g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for g[2],
// using the starting point given by expectedConstant.
let v2 = 0.125;
let t2 = cancelTime + 0.125;
let timeConstant = 0.1;
g[0].gain.setTargetAtTime(v2, t2, timeConstant);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.setTargetAtTime(v2, t2, timeConstant);
return {
constantEndTime: cancelTime + 0.125,
message: "Post setTargetAtTime(" + v2 + ", " + t2 + ", " +
timeConstant + ")",
errorThreshold: 8.4037e-5
};
}).then(task.done.bind(task));
});
audit.define("post cancel: setValue", function (task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule a setTarget after the cancellation.
cancelTest(should, linearRampTest("Post cancel setValueAtTime"), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}, function (g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for g[2],
// using the starting point given by expectedConstant.
let v2 = 0.125;
let t2 = cancelTime + 0.125;
g[0].gain.setValueAtTime(v2, t2);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.setValueAtTime(v2, t2);
return {
constantEndTime: cancelTime + 0.125,
message: "Post setValueAtTime(" + v2 + ", " + t2 + ")"
};
}).then(task.done.bind(task));
});
audit.run();
// Common function for doing a linearRamp test. This just does a linear
// ramp from 0 to v0 at from time 0 to t0. Then another linear ramp is
// scheduled from v0 to 0 from time t0 to t1. This is the ramp that is to
// be cancelled.
function linearRampTest(message) {
return function (g, v0, t0, cancelTime) {
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
let v1 = 0;
let t1 = renderDuration;
g[0].gain.linearRampToValueAtTime(v1, t1);
g[1].gain.linearRampToValueAtTime(v1, t1);
expectedConstant = Math.fround(v0 + (v1 - v0) * (cancelTime - t0) /
(t1 - t0));
return {
expectedConstant: expectedConstant,
autoMessage: "linearRampToValue(" + v1 + ", " + t1 + ")",
summary: message,
};
}
}
// Run the cancellation test. A set of automations is created and
// canceled.
//
// |testFunction| is a function that generates the automation to be
// tested. It is given an array of 3 gain nodes, the value and time of an
// initial linear ramp, and the time where the cancellation should occur.
// The function must do the automations for the first two gain nodes. It
// must return a dictionary with |expectedConstant| being the value at the
// cancellation time, |autoMessage| for message to describe the test, and
// |summary| for general summary message to be printed at the end of the
// test.
//
// |thresholdOptions| is a property bag that specifies the error threshold
// to use. |thresholdOptions.valueThreshold| is the error threshold for
// comparing the actual constant output after cancelling to the expected
// value. |thresholdOptions.curveThreshold| is the error threshold for
// comparing the actual and expected automation curves before the
// cancelation point.
//
// For cancellation tests, |postCancelTest| is a function that schedules
// some automation after the cancellation. It takes 3 arguments: an array
// of the gain nodes, the cancellation time, and the expected value at the
// cancellation time. This function must return a dictionary consisting
// of |constantEndtime| indicating when the held constant from
// cancellation stops being constant, |message| giving a summary of what
// automation is being used, and |errorThreshold| that is the error
// threshold between the expected curve and the actual curve.
//
function cancelTest(should, testerFunction, thresholdOptions,
postCancelTest) {
// Create a context with three channels. Channel 0 is the test channel
// containing the actual output that includes the cancellation of
// events. Channel 1 is the expected data upto the cancellation so we
// can verify the cancellation produced the correct result. Channel 2
// is for verifying events inserted after the cancellation so we can
// verify that automations are correctly generated after the
// cancellation point.
let context = new OfflineAudioContext(3, renderDuration * sampleRate,
sampleRate);
// Test source is a constant signal
let src = context.createBufferSource();
src.buffer = createConstantBuffer(context, 1, 1);
src.loop = true;
// We'll do the automation tests with three gain nodes. One (g0) will
// have cancelValuesAndHoldAtTime and the other (g1) will not. g1 is
// used as the expected result for that automation up to the
// cancellation point. They should be the same. The third node (g2) is
// used for testing automations inserted after the cancellation point,
// if any. g2 is the expected result from the cancellation point to the
// end of the test.
let g0 = context.createGain();
let g1 = context.createGain();
let g2 = context.createGain();
let v0 = 1;
let t0 = 0.01;
let cancelTime = renderDuration / 2;
// Test automation here. The tester function is responsible for setting
// up the gain nodes with the desired automation for testing.
autoResult = testerFunction([g0, g1, g2], v0, t0, cancelTime);
let expectedConstant = autoResult.expectedConstant;
let autoMessage = autoResult.autoMessage;
let summaryMessage = autoResult.summary;
// Cancel scheduled events somewhere in the middle of the test
// automation.
g0.gain.cancelValuesAndHoldAtTime(cancelTime);
let constantEndTime;
if (postCancelTest) {
postResult = postCancelTest([g0, g1, g2], cancelTime,
expectedConstant);
constantEndTime = postResult.constantEndTime;
}
// Connect everything together (with a merger to make a two-channel
// result). Channel 0 is the test (with cancelValuesAndHoldAtTime) and
// channel 1 is the reference (without cancelValuesAndHoldAtTime).
// Channel 1 is used to verify that everything up to the cancellation
// has the correct values.
src.connect(g0);
src.connect(g1);
src.connect(g2);
let merger = context.createChannelMerger(3);
g0.connect(merger, 0, 0);
g1.connect(merger, 0, 1);
g2.connect(merger, 0, 2);
merger.connect(context.destination);
// Go!
src.start();
return context.startRendering().then(function (buffer) {
let actual = buffer.getChannelData(0);
let expected = buffer.getChannelData(1);
// The actual output should be a constant from the cancel time to the
// end. We use the last value of the actual output as the constant,
// but we also want to compare that with what we thought it should
// really be.
let cancelFrame = Math.ceil(cancelTime * sampleRate);
// Verify that the curves up to the cancel time are "identical". The
// should be but round-off may make them differ slightly due to the
// way cancelling is done.
let endFrame = Math.floor(cancelTime * sampleRate);
should(actual.slice(0, endFrame),
autoMessage + " up to time " + cancelTime)
.beCloseToArray(expected.slice(0, endFrame), {
absoluteThreshold: thresholdOptions.curveThreshold
});
// Verify the output after the cancellation is a constant.
let actualTail;
let constantEndFrame;
if (postCancelTest) {
constantEndFrame = Math.ceil(constantEndTime * sampleRate);
actualTail = actual.slice(cancelFrame, constantEndFrame);
} else {
actualTail = actual.slice(cancelFrame);
}
let actualConstant = actual[cancelFrame];
should(actualTail, "Cancelling " + autoMessage + " at time " +
cancelTime)
.beConstantValueOf(actualConstant);
// Verify that the constant is the value we expect.
should(actualConstant, "Expected value for cancelling " +
autoMessage + " at time " +
cancelTime)
.beCloseTo(expectedConstant, {
threshold: thresholdOptions.valueThreshold
});
// Verify the curve after the constantEndTime matches our
// expectations.
if (postCancelTest) {
let c2 = buffer.getChannelData(2);
should(actual.slice(constantEndFrame), postResult.message)
.beCloseToArray(c2.slice(constantEndFrame), {
absoluteThreshold: postResult.errorThreshold || 0
});
}
});
}
</script>
</body>
</html>
...@@ -214,6 +214,7 @@ interface AudioParam ...@@ -214,6 +214,7 @@ interface AudioParam
getter minValue getter minValue
getter value getter value
method cancelScheduledValues method cancelScheduledValues
method cancelValuesAndHoldAtTime
method constructor method constructor
method exponentialRampToValueAtTime method exponentialRampToValueAtTime
method linearRampToValueAtTime method linearRampToValueAtTime
......
...@@ -517,4 +517,11 @@ AudioParam* AudioParam::cancelScheduledValues(double startTime, ...@@ -517,4 +517,11 @@ AudioParam* AudioParam::cancelScheduledValues(double startTime,
return this; return this;
} }
AudioParam* AudioParam::cancelValuesAndHoldAtTime(
double startTime,
ExceptionState& exceptionState) {
handler().timeline().cancelValuesAndHoldAtTime(startTime, exceptionState);
return this;
}
} // namespace blink } // namespace blink
...@@ -251,6 +251,7 @@ class AudioParam final : public GarbageCollectedFinalized<AudioParam>, ...@@ -251,6 +251,7 @@ class AudioParam final : public GarbageCollectedFinalized<AudioParam>,
double duration, double duration,
ExceptionState&); ExceptionState&);
AudioParam* cancelScheduledValues(double startTime, ExceptionState&); AudioParam* cancelScheduledValues(double startTime, ExceptionState&);
AudioParam* cancelValuesAndHoldAtTime(double startTime, ExceptionState&);
private: private:
AudioParam(BaseAudioContext&, AudioParam(BaseAudioContext&,
......
...@@ -50,4 +50,7 @@ interface AudioParam { ...@@ -50,4 +50,7 @@ interface AudioParam {
// Cancels all scheduled parameter changes with times greater than or equal to startTime. // Cancels all scheduled parameter changes with times greater than or equal to startTime.
[RaisesException, MeasureAs=AudioParamCancelScheduledValues] AudioParam cancelScheduledValues(double startTime); [RaisesException, MeasureAs=AudioParamCancelScheduledValues] AudioParam cancelScheduledValues(double startTime);
// Cancel scheduled parameter changes and hold the last value
[RaisesException] AudioParam cancelValuesAndHoldAtTime(double startTime);
}; };
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
#include "platform/audio/AudioUtilities.h" #include "platform/audio/AudioUtilities.h"
#include "wtf/CPU.h" #include "wtf/CPU.h"
#include "wtf/MathExtras.h" #include "wtf/MathExtras.h"
#include "wtf/PtrUtil.h"
#include <algorithm> #include <algorithm>
#if CPU(X86) || CPU(X86_64) #if CPU(X86) || CPU(X86_64)
...@@ -106,6 +107,9 @@ String AudioParamTimeline::eventToString(const ParamEvent& event) { ...@@ -106,6 +107,9 @@ String AudioParamTimeline::eventToString(const ParamEvent& event) {
args = "..., " + String::number(event.time(), 16) + ", " + args = "..., " + String::number(event.time(), 16) + ", " +
String::number(event.duration(), 16); String::number(event.duration(), 16);
break; break;
case ParamEvent::CancelValues:
// Fall through; we should never have to print out the internal
// CancelValues event.
case ParamEvent::LastType: case ParamEvent::LastType:
ASSERT_NOT_REACHED(); ASSERT_NOT_REACHED();
break; break;
...@@ -114,54 +118,77 @@ String AudioParamTimeline::eventToString(const ParamEvent& event) { ...@@ -114,54 +118,77 @@ String AudioParamTimeline::eventToString(const ParamEvent& event) {
return s + "(" + args + ")"; return s + "(" + args + ")";
} }
AudioParamTimeline::ParamEvent::ParamEvent(Type type, // Computes the value of a linear ramp event at time t with the given event
float value, // parameters.
double time, float AudioParamTimeline::linearRampAtTime(double t,
double timeConstant, float value1,
double time1,
float value2,
double time2) {
return value1 + (value2 - value1) * (t - time1) / (time2 - time1);
}
// Computes the value of an exponential ramp event at time t with the given
// event parameters.
float AudioParamTimeline::exponentialRampAtTime(double t,
float value1,
double time1,
float value2,
double time2) {
return value1 * pow(value2 / value1, (t - time1) / (time2 - time1));
}
// Compute the value of a set target event at time t with the given event
// parameters.
float AudioParamTimeline::targetValueAtTime(double t,
float value1,
double time1,
float value2,
float timeConstant) {
return value2 + (value1 - value2) * exp(-(t - time1) / timeConstant);
}
// Compute the value of a set curve event at time t with the given event
// parameters.
float AudioParamTimeline::valueCurveAtTime(double t,
double time1,
double duration, double duration,
const DOMFloat32Array* curve, const float* curveData,
float initialValue, unsigned curveLength) {
double callTime) double curveIndex = (curveLength - 1) / duration * (t - time1);
: m_type(type), unsigned k = std::min(static_cast<unsigned>(curveIndex), curveLength - 1);
m_value(value), unsigned k1 = std::min(k + 1, curveLength - 1);
m_time(time), float c0 = curveData[k];
m_timeConstant(timeConstant), float c1 = curveData[k1];
m_duration(duration), float delta = std::min(curveIndex - k, 1.0);
m_initialValue(initialValue),
m_callTime(callTime), return c0 + (c1 - c0) * delta;
m_needsTimeClampCheck(true) {
if (curve) {
// Copy the curve data
unsigned curveLength = curve->length();
m_curve.resize(curveLength);
memcpy(m_curve.data(), curve->data(), curveLength * sizeof(float));
}
} }
AudioParamTimeline::ParamEvent std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createSetValueEvent(float value, double time) { AudioParamTimeline::ParamEvent::createSetValueEvent(float value, double time) {
return ParamEvent(ParamEvent::SetValue, value, time, 0, 0, nullptr); return WTF::wrapUnique(new ParamEvent(ParamEvent::SetValue, value, time));
} }
AudioParamTimeline::ParamEvent std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createLinearRampEvent(float value, AudioParamTimeline::ParamEvent::createLinearRampEvent(float value,
double time, double time,
float initialValue, float initialValue,
double callTime) { double callTime) {
return ParamEvent(ParamEvent::LinearRampToValue, value, time, 0, 0, nullptr, return WTF::wrapUnique(new ParamEvent(ParamEvent::LinearRampToValue, value,
initialValue, callTime); time, initialValue, callTime));
} }
AudioParamTimeline::ParamEvent std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createExponentialRampEvent(float value, AudioParamTimeline::ParamEvent::createExponentialRampEvent(float value,
double time, double time,
float initialValue, float initialValue,
double callTime) { double callTime) {
return ParamEvent(ParamEvent::ExponentialRampToValue, value, time, 0, 0, return WTF::wrapUnique(new ParamEvent(ParamEvent::ExponentialRampToValue,
nullptr, initialValue, callTime); value, time, initialValue, callTime));
} }
AudioParamTimeline::ParamEvent std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createSetTargetEvent(float value, AudioParamTimeline::ParamEvent::createSetTargetEvent(float value,
double time, double time,
double timeConstant) { double timeConstant) {
...@@ -169,16 +196,211 @@ AudioParamTimeline::ParamEvent::createSetTargetEvent(float value, ...@@ -169,16 +196,211 @@ AudioParamTimeline::ParamEvent::createSetTargetEvent(float value,
// returns NaN or Infinity due to division by zero. The caller // returns NaN or Infinity due to division by zero. The caller
// should have converted this to a SetValueEvent. // should have converted this to a SetValueEvent.
DCHECK_NE(timeConstant, 0); DCHECK_NE(timeConstant, 0);
return ParamEvent(ParamEvent::SetTarget, value, time, timeConstant, 0, return WTF::wrapUnique(
nullptr); new ParamEvent(ParamEvent::SetTarget, value, time, timeConstant));
} }
AudioParamTimeline::ParamEvent std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createSetValueCurveEvent( AudioParamTimeline::ParamEvent::createSetValueCurveEvent(
const DOMFloat32Array* curve, const DOMFloat32Array* curve,
double time, double time,
double duration) { double duration) {
return ParamEvent(ParamEvent::SetValueCurve, 0, time, 0, duration, curve); double curvePoints = (curve->length() - 1) / duration;
float endValue = curve->data()[curve->length() - 1];
return WTF::wrapUnique(new ParamEvent(
ParamEvent::SetValueCurve, time, duration, curve, curvePoints, endValue));
}
std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createCancelValuesEvent(
double time,
std::unique_ptr<ParamEvent> savedEvent) {
if (savedEvent) {
// The savedEvent can only have certain event types. Verify that.
ParamEvent::Type savedType = savedEvent->getType();
DCHECK_NE(savedType, ParamEvent::LastType);
DCHECK(savedType == ParamEvent::LinearRampToValue ||
savedType == ParamEvent::ExponentialRampToValue ||
savedType == ParamEvent::SetValueCurve);
}
return WTF::wrapUnique(
new ParamEvent(ParamEvent::CancelValues, time, std::move(savedEvent)));
}
std::unique_ptr<AudioParamTimeline::ParamEvent>
AudioParamTimeline::ParamEvent::createGeneralEvent(
Type type,
float value,
double time,
float initialValue,
double callTime,
double timeConstant,
double duration,
Vector<float>& curve,
double curvePointsPerSecond,
float curveEndValue,
std::unique_ptr<ParamEvent> savedEvent) {
return WTF::wrapUnique(new ParamEvent(
type, value, time, initialValue, callTime, timeConstant, duration, curve,
curvePointsPerSecond, curveEndValue, std::move(savedEvent)));
}
AudioParamTimeline::ParamEvent* AudioParamTimeline::ParamEvent::savedEvent()
const {
DCHECK_EQ(getType(), ParamEvent::CancelValues);
return m_savedEvent.get();
}
bool AudioParamTimeline::ParamEvent::hasDefaultCancelledValue() const {
DCHECK_EQ(getType(), ParamEvent::CancelValues);
return m_hasDefaultCancelledValue;
}
void AudioParamTimeline::ParamEvent::setCancelledValue(float value) {
DCHECK_EQ(getType(), ParamEvent::CancelValues);
m_value = value;
m_hasDefaultCancelledValue = true;
}
// General event
AudioParamTimeline::ParamEvent::ParamEvent(
ParamEvent::Type type,
float value,
double time,
float initialValue,
double callTime,
double timeConstant,
double duration,
Vector<float>& curve,
double curvePointsPerSecond,
float curveEndValue,
std::unique_ptr<ParamEvent> savedEvent)
: m_type(type),
m_value(value),
m_time(time),
m_initialValue(initialValue),
m_callTime(callTime),
m_timeConstant(timeConstant),
m_duration(duration),
m_curvePointsPerSecond(curvePointsPerSecond),
m_curveEndValue(curveEndValue),
m_savedEvent(std::move(savedEvent)),
m_needsTimeClampCheck(true),
m_hasDefaultCancelledValue(false) {
m_curve = curve;
}
// Create simplest event needing just a value and time, like setValueAtTime
AudioParamTimeline::ParamEvent::ParamEvent(ParamEvent::Type type,
float value,
double time)
: m_type(type),
m_value(value),
m_time(time),
m_initialValue(0),
m_callTime(0),
m_timeConstant(0),
m_duration(0),
m_curvePointsPerSecond(0),
m_curveEndValue(0),
m_savedEvent(nullptr),
m_needsTimeClampCheck(true),
m_hasDefaultCancelledValue(false) {
DCHECK_EQ(type, ParamEvent::SetValue);
}
// Create a linear or exponential ramp that requires an initial value and
// time in case
// there is no actual event that preceeds this event.
AudioParamTimeline::ParamEvent::ParamEvent(ParamEvent::Type type,
float value,
double time,
float initialValue,
double callTime)
: m_type(type),
m_value(value),
m_time(time),
m_initialValue(initialValue),
m_callTime(callTime),
m_timeConstant(0),
m_duration(0),
m_curvePointsPerSecond(0),
m_curveEndValue(0),
m_savedEvent(nullptr),
m_needsTimeClampCheck(true),
m_hasDefaultCancelledValue(false) {
DCHECK(type == ParamEvent::LinearRampToValue ||
type == ParamEvent::ExponentialRampToValue);
}
// Create an event needing a time constant (setTargetAtTime)
AudioParamTimeline::ParamEvent::ParamEvent(ParamEvent::Type type,
float value,
double time,
double timeConstant)
: m_type(type),
m_value(value),
m_time(time),
m_initialValue(0),
m_callTime(0),
m_timeConstant(timeConstant),
m_duration(0),
m_curvePointsPerSecond(0),
m_curveEndValue(0),
m_savedEvent(nullptr),
m_needsTimeClampCheck(true),
m_hasDefaultCancelledValue(false) {
DCHECK_EQ(type, ParamEvent::SetTarget);
}
// Create a setValueCurve event
AudioParamTimeline::ParamEvent::ParamEvent(ParamEvent::Type type,
double time,
double duration,
const DOMFloat32Array* curve,
double curvePointsPerSecond,
float curveEndValue)
: m_type(type),
m_value(0),
m_time(time),
m_initialValue(0),
m_callTime(0),
m_timeConstant(0),
m_duration(duration),
m_curvePointsPerSecond(curvePointsPerSecond),
m_curveEndValue(curveEndValue),
m_savedEvent(nullptr),
m_needsTimeClampCheck(true),
m_hasDefaultCancelledValue(false) {
DCHECK_EQ(type, ParamEvent::SetValueCurve);
if (curve) {
unsigned curveLength = curve->length();
m_curve.resize(curve->length());
memcpy(m_curve.data(), curve->data(), curveLength * sizeof(float));
}
}
// Create CancelValues event
AudioParamTimeline::ParamEvent::ParamEvent(
ParamEvent::Type type,
double time,
std::unique_ptr<ParamEvent> savedEvent)
: m_type(type),
m_value(0),
m_time(time),
m_initialValue(0),
m_callTime(0),
m_timeConstant(0),
m_duration(0),
m_curvePointsPerSecond(0),
m_curveEndValue(0),
m_savedEvent(std::move(savedEvent)),
m_needsTimeClampCheck(true),
m_hasDefaultCancelledValue(false) {
DCHECK_EQ(type, ParamEvent::CancelValues);
} }
void AudioParamTimeline::setValueAtTime(float value, void AudioParamTimeline::setValueAtTime(float value,
...@@ -189,6 +411,7 @@ void AudioParamTimeline::setValueAtTime(float value, ...@@ -189,6 +411,7 @@ void AudioParamTimeline::setValueAtTime(float value,
if (!isNonNegativeAudioParamTime(time, exceptionState)) if (!isNonNegativeAudioParamTime(time, exceptionState))
return; return;
MutexLocker locker(m_eventsLock);
insertEvent(ParamEvent::createSetValueEvent(value, time), exceptionState); insertEvent(ParamEvent::createSetValueEvent(value, time), exceptionState);
} }
...@@ -203,6 +426,7 @@ void AudioParamTimeline::linearRampToValueAtTime( ...@@ -203,6 +426,7 @@ void AudioParamTimeline::linearRampToValueAtTime(
if (!isNonNegativeAudioParamTime(time, exceptionState)) if (!isNonNegativeAudioParamTime(time, exceptionState))
return; return;
MutexLocker locker(m_eventsLock);
insertEvent( insertEvent(
ParamEvent::createLinearRampEvent(value, time, initialValue, callTime), ParamEvent::createLinearRampEvent(value, time, initialValue, callTime),
exceptionState); exceptionState);
...@@ -229,6 +453,7 @@ void AudioParamTimeline::exponentialRampToValueAtTime( ...@@ -229,6 +453,7 @@ void AudioParamTimeline::exponentialRampToValueAtTime(
return; return;
} }
MutexLocker locker(m_eventsLock);
insertEvent(ParamEvent::createExponentialRampEvent(value, time, initialValue, insertEvent(ParamEvent::createExponentialRampEvent(value, time, initialValue,
callTime), callTime),
exceptionState); exceptionState);
...@@ -245,6 +470,8 @@ void AudioParamTimeline::setTargetAtTime(float target, ...@@ -245,6 +470,8 @@ void AudioParamTimeline::setTargetAtTime(float target,
"Time constant")) "Time constant"))
return; return;
MutexLocker locker(m_eventsLock);
// If timeConstant = 0, we instantly jump to the target value, so // If timeConstant = 0, we instantly jump to the target value, so
// insert a SetValueEvent instead of SetTargetEvent. // insert a SetValueEvent instead of SetTargetEvent.
if (timeConstant == 0) { if (timeConstant == 0) {
...@@ -273,6 +500,7 @@ void AudioParamTimeline::setValueCurveAtTime(DOMFloat32Array* curve, ...@@ -273,6 +500,7 @@ void AudioParamTimeline::setValueCurveAtTime(DOMFloat32Array* curve,
return; return;
} }
MutexLocker locker(m_eventsLock);
insertEvent(ParamEvent::createSetValueCurveEvent(curve, time, duration), insertEvent(ParamEvent::createSetValueCurveEvent(curve, time, duration),
exceptionState); exceptionState);
...@@ -284,73 +512,73 @@ void AudioParamTimeline::setValueCurveAtTime(DOMFloat32Array* curve, ...@@ -284,73 +512,73 @@ void AudioParamTimeline::setValueCurveAtTime(DOMFloat32Array* curve,
exceptionState); exceptionState);
} }
void AudioParamTimeline::insertEvent(const ParamEvent& event, void AudioParamTimeline::insertEvent(std::unique_ptr<ParamEvent> event,
ExceptionState& exceptionState) { ExceptionState& exceptionState) {
DCHECK(isMainThread()); DCHECK(isMainThread());
// Sanity check the event. Be super careful we're not getting infected with // Sanity check the event. Be super careful we're not getting infected with
// NaN or Inf. These should have been handled by the caller. // NaN or Inf. These should have been handled by the caller.
bool isValid = event.getType() < ParamEvent::LastType && bool isValid = event->getType() < ParamEvent::LastType &&
std::isfinite(event.value()) && std::isfinite(event.time()) && std::isfinite(event->value()) &&
std::isfinite(event.timeConstant()) && std::isfinite(event->time()) &&
std::isfinite(event.duration()) && event.duration() >= 0; std::isfinite(event->timeConstant()) &&
std::isfinite(event->duration()) && event->duration() >= 0;
DCHECK(isValid); DCHECK(isValid);
if (!isValid) if (!isValid)
return; return;
MutexLocker locker(m_eventsLock);
unsigned i = 0; unsigned i = 0;
double insertTime = event.time(); double insertTime = event->time();
if (!m_events.size() && if (!m_events.size() &&
(event.getType() == ParamEvent::LinearRampToValue || (event->getType() == ParamEvent::LinearRampToValue ||
event.getType() == ParamEvent::ExponentialRampToValue)) { event->getType() == ParamEvent::ExponentialRampToValue)) {
// There are no events preceding these ramps. Insert a new setValueAtTime // There are no events preceding these ramps. Insert a new setValueAtTime
// event to set the starting point for these events. // event to set the starting point for these events.
m_events.insert(0, AudioParamTimeline::ParamEvent::createSetValueEvent( m_events.insert(0, AudioParamTimeline::ParamEvent::createSetValueEvent(
event.initialValue(), event.callTime())); event->initialValue(), event->callTime()));
} }
for (i = 0; i < m_events.size(); ++i) { for (i = 0; i < m_events.size(); ++i) {
if (event.getType() == ParamEvent::SetValueCurve) { if (event->getType() == ParamEvent::SetValueCurve) {
// If this event is a SetValueCurve, make sure it doesn't overlap any // If this event is a SetValueCurve, make sure it doesn't overlap any
// existing event. It's ok if the SetValueCurve starts at the same time as // existing event. It's ok if the SetValueCurve starts at the same time as
// the end of some other duration. // the end of some other duration.
double endTime = event.time() + event.duration(); double endTime = event->time() + event->duration();
if (m_events[i].time() > event.time() && m_events[i].time() < endTime) { if (m_events[i]->time() > event->time() &&
m_events[i]->time() < endTime) {
exceptionState.throwDOMException( exceptionState.throwDOMException(
NotSupportedError, NotSupportedError,
eventToString(event) + " overlaps " + eventToString(m_events[i])); eventToString(*event) + " overlaps " + eventToString(*m_events[i]));
return; return;
} }
} else { } else {
// Otherwise, make sure this event doesn't overlap any existing // Otherwise, make sure this event doesn't overlap any existing
// SetValueCurve event. // SetValueCurve event.
if (m_events[i].getType() == ParamEvent::SetValueCurve) { if (m_events[i]->getType() == ParamEvent::SetValueCurve) {
double endTime = m_events[i].time() + m_events[i].duration(); double endTime = m_events[i]->time() + m_events[i]->duration();
if (event.time() >= m_events[i].time() && event.time() < endTime) { if (event->time() >= m_events[i]->time() && event->time() < endTime) {
exceptionState.throwDOMException( exceptionState.throwDOMException(
NotSupportedError, NotSupportedError, eventToString(*event) + " overlaps " +
eventToString(event) + " overlaps " + eventToString(m_events[i])); eventToString(*m_events[i]));
return; return;
} }
} }
} }
// Overwrite same event type and time. // Overwrite same event type and time.
if (m_events[i].time() == insertTime && if (m_events[i]->time() == insertTime &&
m_events[i].getType() == event.getType()) { m_events[i]->getType() == event->getType()) {
m_events[i] = event; m_events[i] = std::move(event);
return; return;
} }
if (m_events[i].time() > insertTime) if (m_events[i]->time() > insertTime)
break; break;
} }
m_events.insert(i, event); m_events.insert(i, std::move(event));
} }
bool AudioParamTimeline::hasValues() const { bool AudioParamTimeline::hasValues() const {
...@@ -383,13 +611,130 @@ void AudioParamTimeline::cancelScheduledValues(double startTime, ...@@ -383,13 +611,130 @@ void AudioParamTimeline::cancelScheduledValues(double startTime,
// Remove all events starting at startTime. // Remove all events starting at startTime.
for (unsigned i = 0; i < m_events.size(); ++i) { for (unsigned i = 0; i < m_events.size(); ++i) {
if (m_events[i].time() >= startTime) { if (m_events[i]->time() >= startTime) {
m_events.remove(i, m_events.size() - i); m_events.remove(i, m_events.size() - i);
break; break;
} }
} }
} }
void AudioParamTimeline::cancelValuesAndHoldAtTime(
double cancelTime,
ExceptionState& exceptionState) {
DCHECK(isMainThread());
if (!isNonNegativeAudioParamTime(cancelTime, exceptionState))
return;
MutexLocker locker(m_eventsLock);
unsigned i;
// Find the first event at or just past cancelTime.
for (i = 0; i < m_events.size(); ++i) {
if (m_events[i]->time() > cancelTime) {
break;
}
}
// The event that is being cancelled. This is the event just past
// cancelTime, if any.
unsigned cancelledEventIndex = i;
// If the event just before cancelTime is a SetTarget or SetValueCurve
// event, we need to handle that event specially instead of the event after.
if (i > 0 && ((m_events[i - 1]->getType() == ParamEvent::SetTarget) ||
(m_events[i - 1]->getType() == ParamEvent::SetValueCurve))) {
cancelledEventIndex = i - 1;
} else if (i >= m_events.size()) {
// If there were no events occurring after |cancelTime| (and the
// previous event is not SetTarget or SetValueCurve, we're done.
return;
}
// cancelledEvent is the event that is being cancelled.
ParamEvent* cancelledEvent = m_events[cancelledEventIndex].get();
ParamEvent::Type eventType = cancelledEvent->getType();
// New event to be inserted, if any, and a SetValueEvent if needed.
std::unique_ptr<ParamEvent> newEvent = nullptr;
std::unique_ptr<ParamEvent> newSetValueEvent = nullptr;
switch (eventType) {
case ParamEvent::LinearRampToValue:
case ParamEvent::ExponentialRampToValue: {
// For these events we need to remember the parameters of the event
// for a CancelValues event so that we can properly cancel the event
// and hold the value.
std::unique_ptr<ParamEvent> savedEvent = ParamEvent::createGeneralEvent(
eventType, cancelledEvent->value(), cancelledEvent->time(),
cancelledEvent->initialValue(), cancelledEvent->callTime(),
cancelledEvent->timeConstant(), cancelledEvent->duration(),
cancelledEvent->curve(), cancelledEvent->curvePointsPerSecond(),
cancelledEvent->curveEndValue(), nullptr);
newEvent = ParamEvent::createCancelValuesEvent(cancelTime,
std::move(savedEvent));
} break;
case ParamEvent::SetTarget: {
// Don't want to remove the SetTarget event, so bump the index. But
// we do want to insert a cancelEvent so that we stop this
// automation and hold the value when we get there.
++cancelledEventIndex;
newEvent = ParamEvent::createCancelValuesEvent(cancelTime, nullptr);
} break;
case ParamEvent::SetValueCurve: {
double newDuration = cancelTime - cancelledEvent->time();
if (cancelTime > cancelledEvent->time() + cancelledEvent->duration()) {
// If the cancellation time is past the end of the curve,
// there's nothing to do except remove the following events.
++cancelledEventIndex;
} else {
// Cancellation time is in the middle of the curve. Therefore,
// create a new SetValueCurve event with the appropriate new
// parameters to cancel this event properly. Since it's illegal
// to insert any event within a SetValueCurve event, we can
// compute the new end value now instead of doing when running
// the timeline.
float endValue = valueCurveAtTime(
cancelTime, cancelledEvent->time(), cancelledEvent->duration(),
cancelledEvent->curve().data(), cancelledEvent->curve().size());
// Replace the existing SetValueCurve with this new one that is
// identical except for the duration.
newEvent = ParamEvent::createGeneralEvent(
eventType, cancelledEvent->value(), cancelledEvent->time(),
cancelledEvent->initialValue(), cancelledEvent->callTime(),
cancelledEvent->timeConstant(), newDuration,
cancelledEvent->curve(), cancelledEvent->curvePointsPerSecond(),
endValue, nullptr);
newSetValueEvent = ParamEvent::createSetValueEvent(
endValue, cancelledEvent->time() + newDuration);
}
} break;
case ParamEvent::SetValue:
case ParamEvent::CancelValues:
// Nothing needs to be done for a SetValue or CancelValues event.
break;
case ParamEvent::LastType:
NOTREACHED();
break;
}
// Now remove all the following events from the timeline.
if (cancelledEventIndex < m_events.size())
m_events.remove(cancelledEventIndex, m_events.size() - cancelledEventIndex);
// Insert the new event, if any.
if (newEvent) {
insertEvent(std::move(newEvent), exceptionState);
if (newSetValueEvent)
insertEvent(std::move(newSetValueEvent), exceptionState);
}
}
float AudioParamTimeline::valueForContextTime( float AudioParamTimeline::valueForContextTime(
AudioDestinationHandler& audioDestination, AudioDestinationHandler& audioDestination,
float defaultValue, float defaultValue,
...@@ -399,7 +744,7 @@ float AudioParamTimeline::valueForContextTime( ...@@ -399,7 +744,7 @@ float AudioParamTimeline::valueForContextTime(
{ {
MutexTryLocker tryLocker(m_eventsLock); MutexTryLocker tryLocker(m_eventsLock);
if (!tryLocker.locked() || !m_events.size() || if (!tryLocker.locked() || !m_events.size() ||
audioDestination.currentTime() < m_events[0].time()) { audioDestination.currentTime() < m_events[0]->time()) {
hasValue = false; hasValue = false;
return defaultValue; return defaultValue;
} }
...@@ -462,7 +807,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -462,7 +807,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// Return default value if there are no events matching the desired time // Return default value if there are no events matching the desired time
// range. // range.
if (!m_events.size() || (endFrame / sampleRate <= m_events[0].time())) { if (!m_events.size() || (endFrame / sampleRate <= m_events[0]->time())) {
for (unsigned i = 0; i < numberOfValues; ++i) for (unsigned i = 0; i < numberOfValues; ++i)
values[i] = defaultValue; values[i] = defaultValue;
return defaultValue; return defaultValue;
...@@ -477,20 +822,20 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -477,20 +822,20 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// Look at all the events in the timeline and check to see if any needs // Look at all the events in the timeline and check to see if any needs
// to clamp the start time to the current time. // to clamp the start time to the current time.
for (int k = 0; k < numberOfEvents; ++k) { for (int k = 0; k < numberOfEvents; ++k) {
ParamEvent& event = m_events[k]; ParamEvent* event = m_events[k].get();
// We're examining the event for the first time and the event time is // We're examining the event for the first time and the event time is
// in the past so clamp the event time to the current time (start of // in the past so clamp the event time to the current time (start of
// the rendering quantum). // the rendering quantum).
if (event.needsTimeClampCheck()) { if (event->needsTimeClampCheck()) {
if (event.time() < currentTime) { if (event->time() < currentTime) {
event.setTime(currentTime); event->setTime(currentTime);
clampedSomeEventTime = true; clampedSomeEventTime = true;
} }
// In all cases, we can clear the flag because the event is either // In all cases, we can clear the flag because the event is either
// in the future, or we've already checked it (just now). // in the future, or we've already checked it (just now).
event.clearTimeClampCheck(); event->clearTimeClampCheck();
} }
} }
...@@ -503,9 +848,9 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -503,9 +848,9 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
} }
// Optimize the case where the last event is in the past. // Optimize the case where the last event is in the past.
ParamEvent& lastEvent = m_events[m_events.size() - 1]; ParamEvent* lastEvent = m_events[m_events.size() - 1].get();
ParamEvent::Type lastEventType = lastEvent.getType(); ParamEvent::Type lastEventType = lastEvent->getType();
double lastEventTime = lastEvent.time(); double lastEventTime = lastEvent->time();
// If the last event is in the past and the event has ended, then we can // If the last event is in the past and the event has ended, then we can
// just propagate the same value. Except for SetTarget which lasts // just propagate the same value. Except for SetTarget which lasts
...@@ -531,7 +876,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -531,7 +876,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// If first event is after startFrame then fill initial part of values buffer // If first event is after startFrame then fill initial part of values buffer
// with defaultValue until we reach the first event time. // with defaultValue until we reach the first event time.
double firstEventTime = m_events[0].time(); double firstEventTime = m_events[0]->time();
if (firstEventTime > startFrame / sampleRate) { if (firstEventTime > startFrame / sampleRate) {
// |fillToFrame| is an exclusive upper bound, so use ceil() to compute the // |fillToFrame| is an exclusive upper bound, so use ceil() to compute the
// bound from the firstEventTime. // bound from the firstEventTime.
...@@ -555,8 +900,8 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -555,8 +900,8 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// stopping when we've rendered all the requested values. // stopping when we've rendered all the requested values.
int lastSkippedEventIndex = 0; int lastSkippedEventIndex = 0;
for (int i = 0; i < numberOfEvents && writeIndex < numberOfValues; ++i) { for (int i = 0; i < numberOfEvents && writeIndex < numberOfValues; ++i) {
ParamEvent& event = m_events[i]; ParamEvent* event = m_events[i].get();
ParamEvent* nextEvent = i < numberOfEvents - 1 ? &(m_events[i + 1]) : 0; ParamEvent* nextEvent = i < numberOfEvents - 1 ? m_events[i + 1].get() : 0;
// Wait until we get a more recent event. // Wait until we get a more recent event.
// //
...@@ -579,12 +924,12 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -579,12 +924,12 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// and not applied, which is wrong. Other events don't have this problem. // and not applied, which is wrong. Other events don't have this problem.
// (Because currentFrame is unsigned, we do the time check in this funny, // (Because currentFrame is unsigned, we do the time check in this funny,
// but equivalent way.) // but equivalent way.)
double eventFrame = event.time() * sampleRate; double eventFrame = event->time() * sampleRate;
// Condition is currentFrame - 1 < eventFrame <= currentFrame, but // Condition is currentFrame - 1 < eventFrame <= currentFrame, but
// currentFrame is unsigned and could be 0, so use // currentFrame is unsigned and could be 0, so use
// currentFrame < eventFrame + 1 instead. // currentFrame < eventFrame + 1 instead.
if (!((event.getType() == ParamEvent::SetValue && if (!((event->getType() == ParamEvent::SetValue &&
(eventFrame <= currentFrame) && (eventFrame <= currentFrame) &&
(currentFrame < eventFrame + 1)))) { (currentFrame < eventFrame + 1)))) {
// This is not the special SetValue event case, and nextEvent is // This is not the special SetValue event case, and nextEvent is
...@@ -605,7 +950,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -605,7 +950,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// LinearRampToValue or ExponentialRampToValue, special handling is needed. // LinearRampToValue or ExponentialRampToValue, special handling is needed.
// In this case, the linear and exponential ramp should start at wherever // In this case, the linear and exponential ramp should start at wherever
// the SetTarget processing has reached. // the SetTarget processing has reached.
if (event.getType() == ParamEvent::SetTarget && if (event->getType() == ParamEvent::SetTarget &&
(nextEventType == ParamEvent::LinearRampToValue || (nextEventType == ParamEvent::LinearRampToValue ||
nextEventType == ParamEvent::ExponentialRampToValue)) { nextEventType == ParamEvent::ExponentialRampToValue)) {
// Replace the SetTarget with a SetValue to set the starting time and // Replace the SetTarget with a SetValue to set the starting time and
...@@ -627,36 +972,111 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -627,36 +972,111 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// -2 <= 2 * Fs * t0 - 2 * f <= 0 // -2 <= 2 * Fs * t0 - 2 * f <= 0
// -1 <= 2 * Fs * t0 - 2 * f + 1 <= 1 // -1 <= 2 * Fs * t0 - 2 * f + 1 <= 1
// abs(2 * Fs * t0 - 2 * f + 1) <= 1 // abs(2 * Fs * t0 - 2 * f + 1) <= 1
if (fabs(2 * sampleRate * event.time() - 2 * currentFrame + 1) <= 1) { if (fabs(2 * sampleRate * event->time() - 2 * currentFrame + 1) <= 1) {
// SetTarget is starting somewhere between currentFrame - 1 and // SetTarget is starting somewhere between currentFrame - 1 and
// currentFrame. Compute the value the SetTarget would have at the // currentFrame. Compute the value the SetTarget would have at the
// currentFrame. // currentFrame.
value = event.value() + value = event->value() +
(value - event.value()) * (value - event->value()) *
exp(-(currentFrame / sampleRate - event.time()) / exp(-(currentFrame / sampleRate - event->time()) /
event.timeConstant()); event->timeConstant());
} else { } else {
// SetTarget has already started. Update |value| one frame because it's // SetTarget has already started. Update |value| one frame because it's
// the value from the previous frame. // the value from the previous frame.
float discreteTimeConstant = static_cast<float>( float discreteTimeConstant = static_cast<float>(
AudioUtilities::discreteTimeConstantForSampleRate( AudioUtilities::discreteTimeConstantForSampleRate(
event.timeConstant(), controlRate)); event->timeConstant(), controlRate));
value += (event.value() - value) * discreteTimeConstant; value += (event->value() - value) * discreteTimeConstant;
} }
// Insert a SetValueEvent to mark the starting value and time. // Insert a SetValueEvent to mark the starting value and time.
// Clear the clamp check because this doesn't need it. // Clear the clamp check because this doesn't need it.
m_events[i] = m_events[i] =
ParamEvent::createSetValueEvent(value, currentFrame / sampleRate); ParamEvent::createSetValueEvent(value, currentFrame / sampleRate);
m_events[i].clearTimeClampCheck(); m_events[i]->clearTimeClampCheck();
// Update our pointer to the current event because we just changed it.
event = m_events[i].get();
} }
float value1 = event.value(); float value1 = event->value();
double time1 = event.time(); double time1 = event->time();
float value2 = nextEvent ? nextEvent->value() : value1; float value2 = nextEvent ? nextEvent->value() : value1;
double time2 = nextEvent ? nextEvent->time() : endFrame / sampleRate + 1; double time2 = nextEvent ? nextEvent->time() : endFrame / sampleRate + 1;
// Check to see if an event was cancelled.
if (nextEventType == ParamEvent::CancelValues) {
switch (event->getType()) {
case ParamEvent::LinearRampToValue:
case ParamEvent::ExponentialRampToValue:
case ParamEvent::SetValue: {
// These three events potentially establish a starting value for
// the following event, so we need to examine the cancelled
// event to see what to do.
const ParamEvent* savedEvent = nextEvent->savedEvent();
// Update the end time and type to pretend that we're running
// this saved event type.
time2 = nextEvent->time();
nextEventType = savedEvent->getType();
if (nextEvent->hasDefaultCancelledValue()) {
// We've already established a value for the cancelled
// event, so just return it.
value2 = nextEvent->value();
} else {
// If the next event would have been a LinearRamp or
// ExponentialRamp, we need to compute a new end value for
// the event so that the curve works continues as if it were
// not cancelled.
switch (savedEvent->getType()) {
case ParamEvent::LinearRampToValue:
value2 =
linearRampAtTime(nextEvent->time(), value1, time1,
savedEvent->value(), savedEvent->time());
break;
case ParamEvent::ExponentialRampToValue:
value2 = exponentialRampAtTime(nextEvent->time(), value1, time1,
savedEvent->value(),
savedEvent->time());
break;
case ParamEvent::SetValueCurve:
case ParamEvent::SetValue:
case ParamEvent::SetTarget:
case ParamEvent::CancelValues:
// These cannot be possible types for the saved event
// because they can't be created.
// createCancelValuesEvent doesn't allow them (SetValue,
// SetTarget, CancelValues) or cancelScheduledValues()
// doesn't create such an event (SetValueCurve).
NOTREACHED();
break;
case ParamEvent::LastType:
// Illegal event type.
NOTREACHED();
break;
}
// Cache the new value so we don't keep computing it over and over.
nextEvent->setCancelledValue(value2);
}
} break;
case ParamEvent::SetValueCurve:
// Everything needed for this was handled when cancelling was
// done.
break;
case ParamEvent::SetTarget:
case ParamEvent::CancelValues:
// Nothing special needs to be done for SetTarget or
// CancelValues followed by CancelValues.
break;
case ParamEvent::LastType:
NOTREACHED();
break;
}
}
DCHECK_GE(time2, time1); DCHECK_GE(time2, time1);
double deltaTime = time2 - time1; double deltaTime = time2 - time1;
float k = deltaTime > 0 ? 1 / deltaTime : 0; float k = deltaTime > 0 ? 1 / deltaTime : 0;
...@@ -788,13 +1208,13 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -788,13 +1208,13 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
} }
} else { } else {
// Handle event types not requiring looking ahead to the next event. // Handle event types not requiring looking ahead to the next event.
switch (event.getType()) { switch (event->getType()) {
case ParamEvent::SetValue: case ParamEvent::SetValue:
case ParamEvent::LinearRampToValue: { case ParamEvent::LinearRampToValue: {
currentFrame = fillToEndFrame; currentFrame = fillToEndFrame;
// Simply stay at a constant value. // Simply stay at a constant value.
value = event.value(); value = event->value();
for (; writeIndex < fillToFrame; ++writeIndex) for (; writeIndex < fillToFrame; ++writeIndex)
values[writeIndex] = value; values[writeIndex] = value;
...@@ -802,6 +1222,37 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -802,6 +1222,37 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
break; break;
} }
case ParamEvent::CancelValues: {
// If the previous event was a SetTarget or ExponentialRamp
// event, the current value is one sample behind. Update
// the sample value by one sample, but only at the start of
// this CancelValues event.
if (event->hasDefaultCancelledValue()) {
value = event->value();
} else {
double cancelFrame = time1 * sampleRate;
if (i >= 1 && cancelFrame <= currentFrame &&
currentFrame < cancelFrame + 1) {
ParamEvent::Type lastEventType = m_events[i - 1]->getType();
if (lastEventType == ParamEvent::SetTarget) {
float target = m_events[i - 1]->value();
float timeConstant = m_events[i - 1]->timeConstant();
float discreteTimeConstant = static_cast<float>(
AudioUtilities::discreteTimeConstantForSampleRate(
timeConstant, controlRate));
value += (target - value) * discreteTimeConstant;
}
}
}
// Simply stay at the current value.
for (; writeIndex < fillToFrame; ++writeIndex)
values[writeIndex] = value;
currentFrame = fillToEndFrame;
break;
}
case ParamEvent::ExponentialRampToValue: { case ParamEvent::ExponentialRampToValue: {
currentFrame = fillToEndFrame; currentFrame = fillToEndFrame;
...@@ -809,8 +1260,8 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -809,8 +1260,8 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// (because the start and end values have the same sign, and neither // (because the start and end values have the same sign, and neither
// is 0), use the actual end value. If not, we have to propagate // is 0), use the actual end value. If not, we have to propagate
// whatever we have. // whatever we have.
if (i >= 1 && ((m_events[i - 1].value() * event.value()) > 0)) if (i >= 1 && ((m_events[i - 1]->value() * event->value()) > 0))
value = event.value(); value = event->value();
// Simply stay at a constant value from the last time. We don't want // Simply stay at a constant value from the last time. We don't want
// to use the value of the event in case value1 * value2 < 0. In this // to use the value of the event in case value1 * value2 < 0. In this
...@@ -827,8 +1278,8 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -827,8 +1278,8 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// v(t) = v2 + (v1 - v2)*exp(-(t-t1/tau)) // v(t) = v2 + (v1 - v2)*exp(-(t-t1/tau))
// //
float target = event.value(); float target = event->value();
float timeConstant = event.timeConstant(); float timeConstant = event->timeConstant();
float discreteTimeConstant = static_cast<float>( float discreteTimeConstant = static_cast<float>(
AudioUtilities::discreteTimeConstantForSampleRate(timeConstant, AudioUtilities::discreteTimeConstantForSampleRate(timeConstant,
controlRate)); controlRate));
...@@ -917,16 +1368,18 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -917,16 +1368,18 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
} }
case ParamEvent::SetValueCurve: { case ParamEvent::SetValueCurve: {
Vector<float> curve = event.curve(); Vector<float> curve = event->curve();
float* curveData = curve.data(); float* curveData = curve.data();
unsigned numberOfCurvePoints = curve.size(); unsigned numberOfCurvePoints = curve.size();
float curveEndValue = event->curveEndValue();
// Curve events have duration, so don't just use next event time. // Curve events have duration, so don't just use next event time.
double duration = event.duration(); double duration = event->duration();
// How much to step the curve index for each frame. This is basically // How much to step the curve index for each frame. This is basically
// the term (N - 1)/Td in the specification. // the term (N - 1)/Td in the specification.
double curvePointsPerFrame = double curvePointsPerFrame =
(numberOfCurvePoints - 1) / duration / sampleRate; event->curvePointsPerSecond() / sampleRate;
if (!numberOfCurvePoints || duration <= 0 || sampleRate <= 0) { if (!numberOfCurvePoints || duration <= 0 || sampleRate <= 0) {
// Error condition - simply propagate previous value. // Error condition - simply propagate previous value.
...@@ -976,7 +1429,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -976,7 +1429,7 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
} }
// Set the default value in case fillToFrame is 0. // Set the default value in case fillToFrame is 0.
value = curveData[numberOfCurvePoints - 1]; value = curveEndValue;
// Render the stretched curve data using linear interpolation. // Render the stretched curve data using linear interpolation.
// Oversampled curve data can be provided if sharp discontinuities are // Oversampled curve data can be provided if sharp discontinuities are
...@@ -1079,11 +1532,12 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, ...@@ -1079,11 +1532,12 @@ float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame,
// If there's any time left after the duration of this event and the // If there's any time left after the duration of this event and the
// start of the next, then just propagate the last value of the // start of the next, then just propagate the last value of the
// curveData. // curveData. Don't modify |value| unless there is time left.
if (writeIndex < nextEventFillToFrame) if (writeIndex < nextEventFillToFrame) {
value = curveData[numberOfCurvePoints - 1]; value = curveEndValue;
for (; writeIndex < nextEventFillToFrame; ++writeIndex) for (; writeIndex < nextEventFillToFrame; ++writeIndex)
values[writeIndex] = value; values[writeIndex] = value;
}
// Re-adjust current time // Re-adjust current time
currentFrame += nextEventFillToFrame; currentFrame += nextEventFillToFrame;
......
...@@ -64,6 +64,7 @@ class AudioParamTimeline { ...@@ -64,6 +64,7 @@ class AudioParamTimeline {
double duration, double duration,
ExceptionState&); ExceptionState&);
void cancelScheduledValues(double startTime, ExceptionState&); void cancelScheduledValues(double startTime, ExceptionState&);
void cancelValuesAndHoldAtTime(double cancelTime, ExceptionState&);
// hasValue is set to true if a valid timeline value is returned. // hasValue is set to true if a valid timeline value is returned.
// otherwise defaultValue is returned. // otherwise defaultValue is returned.
...@@ -105,27 +106,50 @@ class AudioParamTimeline { ...@@ -105,27 +106,50 @@ class AudioParamTimeline {
ExponentialRampToValue, ExponentialRampToValue,
SetTarget, SetTarget,
SetValueCurve, SetValueCurve,
// For cancelValuesAndHold
CancelValues,
LastType LastType
}; };
static ParamEvent createLinearRampEvent(float value, static std::unique_ptr<ParamEvent> createLinearRampEvent(float value,
double time, double time,
float initialValue, float initialValue,
double callTime); double callTime);
static ParamEvent createExponentialRampEvent(float value, static std::unique_ptr<ParamEvent> createExponentialRampEvent(
float value,
double time, double time,
float initialValue, float initialValue,
double callTime); double callTime);
static ParamEvent createSetValueEvent(float value, double time); static std::unique_ptr<ParamEvent> createSetValueEvent(float value,
static ParamEvent createSetTargetEvent(float value, double time);
double time, static std::unique_ptr<ParamEvent>
double timeConstant); createSetTargetEvent(float value, double time, double timeConstant);
static ParamEvent createSetValueCurveEvent(const DOMFloat32Array* curve, static std::unique_ptr<ParamEvent> createSetValueCurveEvent(
const DOMFloat32Array* curve,
double time, double time,
double duration); double duration);
static std::unique_ptr<ParamEvent> createCancelValuesEvent(
double time,
std::unique_ptr<ParamEvent> savedEvent);
// Needed for creating a saved event where we want to supply all
// the possible parameters because we're mostly copying an
// existing event.
static std::unique_ptr<ParamEvent> createGeneralEvent(
Type,
float value,
double time,
float initialValue,
double callTime,
double timeConstant,
double duration,
Vector<float>& curve,
double curvePointsPerSecond,
float curveEndValue,
std::unique_ptr<ParamEvent> savedEvent);
static bool eventPreceeds(const ParamEvent& a, const ParamEvent& b) { static bool eventPreceeds(const std::unique_ptr<ParamEvent>& a,
return a.time() < b.time(); const std::unique_ptr<ParamEvent>& b) {
return a->time() < b->time();
} }
Type getType() const { return m_type; } Type getType() const { return m_type; }
...@@ -140,34 +164,104 @@ class AudioParamTimeline { ...@@ -140,34 +164,104 @@ class AudioParamTimeline {
bool needsTimeClampCheck() const { return m_needsTimeClampCheck; } bool needsTimeClampCheck() const { return m_needsTimeClampCheck; }
void clearTimeClampCheck() { m_needsTimeClampCheck = false; } void clearTimeClampCheck() { m_needsTimeClampCheck = false; }
double curvePointsPerSecond() const { return m_curvePointsPerSecond; }
float curveEndValue() const { return m_curveEndValue; }
// For CancelValues events. Not valid for any other event.
ParamEvent* savedEvent() const;
bool hasDefaultCancelledValue() const;
void setCancelledValue(float);
private: private:
// General event
ParamEvent(Type type, ParamEvent(Type type,
float value, float value,
double time, double time,
float initialValue,
double callTime,
double timeConstant, double timeConstant,
double duration, double duration,
Vector<float>& curve,
double curvePointsPerSecond,
float curveEndValue,
std::unique_ptr<ParamEvent> savedEvent);
// Create simplest event needing just a value and time, like
// setValueAtTime.
ParamEvent(Type, float value, double time);
// Create a linear or exponential ramp that requires an initial
// value and time in case there is no actual event that preceeds
// this event.
ParamEvent(Type,
float value,
double time,
float initialValue,
double callTime);
// Create an event needing a time constant (setTargetAtTime)
ParamEvent(Type, float value, double time, double timeConstant);
// Create a setValueCurve event
ParamEvent(Type,
double time,
double duration,
const DOMFloat32Array* curve, const DOMFloat32Array* curve,
float initialValue = 0, double curvePointsPerSecond,
double callTime = 0); float curveEndValue);
// Create CancelValues event
ParamEvent(Type, double time, std::unique_ptr<ParamEvent> savedEvent);
Type m_type; Type m_type;
// The value for the event. The interpretation of this depends on
// the event type. Not used for SetValueCurve. For CancelValues,
// it is the end value to use when cancelling a LinearRampToValue
// or ExponentialRampToValue event.
float m_value; float m_value;
// The time for the event. The interpretation of this depends on
// the event type.
double m_time; double m_time;
// Only used for SetTarget events
double m_timeConstant;
// Only used for SetValueCurve events.
double m_duration;
Vector<float> m_curve;
// Initial value and time to use for linear and exponential ramps that don't // Initial value and time to use for linear and exponential ramps that don't
// have a preceding event. // have a preceding event.
float m_initialValue; float m_initialValue;
double m_callTime; double m_callTime;
// Only used for SetTarget events
double m_timeConstant;
// The following items are only used for SetValueCurve events.
//
// The duration of the curve.
double m_duration;
// The array of curve points.
Vector<float> m_curve;
// The number of curve points per second. it is used to compute
// the curve index step when running the automation.
double m_curvePointsPerSecond;
// The default value to use at the end of the curve. Normally
// it's the last entry in m_curve, but cancelling a SetValueCurve
// will set this to a new value.
float m_curveEndValue;
// For CancelValues. If CancelValues is in the middle of an event, this
// holds the event that is being cancelled, so that processing can
// continue as if the event still existed up until we reach the actual
// scheduled cancel time.
std::unique_ptr<ParamEvent> m_savedEvent;
// True if the start time needs to be checked against current time // True if the start time needs to be checked against current time
// to implement clamping. // to implement clamping.
bool m_needsTimeClampCheck; bool m_needsTimeClampCheck;
// True if a default value has been assigned to the CancelValues event.
bool m_hasDefaultCancelledValue;
}; };
void insertEvent(const ParamEvent&, ExceptionState&); void insertEvent(std::unique_ptr<ParamEvent>, ExceptionState&);
float valuesForFrameRangeImpl(size_t startFrame, float valuesForFrameRangeImpl(size_t startFrame,
size_t endFrame, size_t endFrame,
float defaultValue, float defaultValue,
...@@ -178,7 +272,33 @@ class AudioParamTimeline { ...@@ -178,7 +272,33 @@ class AudioParamTimeline {
// Produce a nice string describing the event in human-readable form. // Produce a nice string describing the event in human-readable form.
String eventToString(const ParamEvent&); String eventToString(const ParamEvent&);
Vector<ParamEvent> m_events;
// Automation functions that compute the vlaue of the specified
// automation at the specified time.
float linearRampAtTime(double t,
float value1,
double time1,
float value2,
double time2);
float exponentialRampAtTime(double t,
float value1,
double time1,
float value2,
double time2);
float targetValueAtTime(double t,
float value1,
double time1,
float value2,
float timeConstant);
float valueCurveAtTime(double t,
double time1,
double duration,
const float* curveData,
unsigned curveLength);
// Vector of all automation events for the AudioParam. Access must
// be locked via m_eventsLock.
Vector<std::unique_ptr<ParamEvent>> m_events;
mutable Mutex m_eventsLock; mutable Mutex m_eventsLock;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment