Make setTarget followed by linear or exponential ramp continuous.
When setTarget is followed by a linear or exponential ramp, the
current behavior produces discontinuous curves. In this case, make
the curve continuous by inserting a setValue to freeze the curve at
the last value of the setTarget. This establishes the starting point
for the following linear and exponential ramp to make it continuous.
WebAudio issue: https://github.com/WebAudio/web-audio-api/issues/652
Spec proposal: https://github.com/WebAudio/web-audio-api/pull/665
BUG=564157
TEST=audioparam-setTargetAtTime-continuous.html
Review-Url: https://codereview.chromium.org/1485003002
Cr-Commit-Position: refs/heads/master@{#391597}
diff --git a/third_party/WebKit/LayoutTests/webaudio/audioparam-setTargetAtTime-continuous-expected.txt b/third_party/WebKit/LayoutTests/webaudio/audioparam-setTargetAtTime-continuous-expected.txt
new file mode 100644
index 0000000..df6bcfb
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/webaudio/audioparam-setTargetAtTime-continuous-expected.txt
@@ -0,0 +1,33 @@
+Test SetTarget Followed by Linear or Exponential Ramp
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Linear ramp: Initial part equals [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,...] with an element-wise tolerance of 0.
+PASS SetTarget part was correctly replaced by the ramp
+PASS Linear ramp equals [1,1.0004401408450705,1.0008802816901408,1.0013204225352113,1.0017605633802817,1.002200704225352,1.0026408450704225,1.003080985915493,1.0035211267605635,1.0039612676056338,1.0044014084507042,1.0048415492957747,1.005281690140845,1.0057218309859155,1.006161971830986,1.0066021126760563,...] with an element-wise tolerance of 0.00000126765.
+PASS Linear ramp: Tail part equals [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,...] with an element-wise tolerance of 0.
+PASS Linear ramp preceded by SetTarget is continuous.
+
+PASS Delayed linear ramp: Initial part equals [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,...] with an element-wise tolerance of 0.
+PASS Delayed linear ramp: SetTarget part equals [1,0.9979188352992993,0.9958420018451098,0.9937694906233947,0.9917012926388759,0.9896373989149966,0.9875778004938814,0.985522488436298,0.9834714538216174,0.981424687747777,0.9793821813312401,0.9773439257069583,0.9753099120283326,0.9732801314671756,0.9712545752136729,0.969233234476344,...] with an element-wise tolerance of 2.19417e-7.
+PASS Delayed linear ramp equals [0.4493289641172215,0.4501502941150408,0.45097162411286007,0.4517929541106793,0.4526142841084986,0.4534356141063179,0.45425694410413714,0.4550782741019564,0.4558996040997757,0.45672093409759495,0.45754226409541415,0.45836359409323346,0.4591849240910527,0.46000625408887197,0.4608275840866913,0.46164891408451053,...] with an element-wise tolerance of 0.00000107972.
+PASS Delayed linear ramp: Tail part equals [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,...] with an element-wise tolerance of 0.
+PASS Delayed linear ramp preceded by SetTarget is continuous.
+
+PASS Exponential ramp: Initial part equals [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,...] with an element-wise tolerance of 0.
+PASS SetTarget part was correctly replaced by the ramp
+PASS Exponential ramp equals [1,1.00030517578125,1.0006103515625,1.0009156465530396,1.0012210607528687,1.0015265941619873,1.001832127571106,1.0021378993988037,1.0024436712265015,1.0027495622634888,1.003055453300476,1.0033615827560425,1.0036677122116089,1.0039739608764648,1.0042803287506104,1.0045866966247559,...] with an element-wise tolerance of 0.0000113249.
+PASS Exponential ramp: Tail part equals [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,...] with an element-wise tolerance of 0.
+PASS Exponential ramp preceded by SetTarget is continuous.
+
+PASS Delayed exponential ramp: Initial part equals [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,...] with an element-wise tolerance of 0.
+PASS Delayed exponential ramp: SetTarget part equals [1,0.9979188352992993,0.9958420018451098,0.9937694906233947,0.9917012926388759,0.9896373989149966,0.9875778004938814,0.985522488436298,0.9834714538216174,0.981424687747777,0.9793821813312401,0.9773439257069583,0.9753099120283326,0.9732801314671756,0.9712545752136729,0.969233234476344,...] with an element-wise tolerance of 2.19417e-7.
+PASS Delayed exponential ramp equals [0.4493289589881897,0.4496844708919525,0.45004022121429443,0.4503962993621826,0.4507526457309723,0.45110926032066345,0.4514661729335785,0.451823353767395,0.4521808326244354,0.4525385797023773,0.4528966248035431,0.45325493812561035,0.4536135494709015,0.4539724290370941,0.4543316066265106,0.4546910524368286,...] with an element-wise tolerance of 0.00000417233.
+PASS Delayed exponential ramp: Tail part equals [2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,...] with an element-wise tolerance of 0.
+PASS Delayed exponential ramp preceded by SetTarget is continuous.
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/third_party/WebKit/LayoutTests/webaudio/audioparam-setTargetAtTime-continuous.html b/third_party/WebKit/LayoutTests/webaudio/audioparam-setTargetAtTime-continuous.html
new file mode 100644
index 0000000..f5bbf22
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/webaudio/audioparam-setTargetAtTime-continuous.html
@@ -0,0 +1,235 @@
+<!doctype html>
+<html>
+ <head>
+ <script src="../resources/js-test.js"></script>
+ <script src="resources/compatibility.js"></script>
+ <script src="resources/audio-testing.js"></script>
+ <script src="resources/audioparam-testing.js"></script>
+ <title>SetTarget Followed by Linear or Exponential Ramp Is Continuous</title>
+ </head>
+
+ <body>
+ <script>
+ description("Test SetTarget Followed by Linear or Exponential Ramp");
+ window.jsTestIsAsync = true;
+
+ var sampleRate = 48000;
+ var renderQuantum = 128;
+ // Test doesn't need to run for very long.
+ var renderDuration = 0.1;
+ // Where the ramp should end
+ var rampEndTime = renderDuration - .05;
+ var renderFrames = renderDuration * sampleRate;
+ var timeConstant = 0.01;
+
+ var audit = Audit.createTaskRunner();
+
+ // All of the tests start a SetTargetAtTime after one rendering quantum. The following tests
+ // handle various cases where a linear or exponential ramp is scheduled at or after
+ // SetTargetAtTime starts.
+
+ audit.defineTask("linear ramp replace", function (done) {
+ // Schedule a linear ramp to start at the same time as SetTargetAtTime. This effectively
+ // replaces the SetTargetAtTime as if it never existed.
+ runTest("Linear ramp", {
+ automationFunction: function (audioparam, endValue, endTime) {
+ audioparam.linearRampToValueAtTime(endValue, endTime);
+ },
+ referenceFunction: linearResult,
+ automationTime: renderQuantum / sampleRate,
+ thresholdSetTarget: 0,
+ thresholdRamp: 1.26765e-6
+ }).then(done);
+ });
+
+ audit.defineTask("delayed linear ramp", function (done) {
+ // Schedule a linear ramp to start after the SetTargetAtTime has already started rendering.
+ // This is the main test to verify that the linear ramp is continuous with the
+ // SetTargetAtTime curve.
+ runTest("Delayed linear ramp", {
+ automationFunction: function (audioparam, endValue, endTime) {
+ audioparam.linearRampToValueAtTime(endValue, endTime);
+ },
+ referenceFunction: linearResult,
+ automationTime: 4 * renderQuantum / sampleRate,
+ thresholdSetTarget: 2.19417e-7,
+ thresholdRamp: 1.07972e-6
+ }).then(done);
+ });
+
+ audit.defineTask("expo ramp replace", function (done) {
+ // Like "linear ramp replace", but with an exponential ramp instead.
+ runTest("Exponential ramp", {
+ automationFunction: function (audioparam, endValue, endTime) {
+ audioparam.exponentialRampToValueAtTime(endValue, endTime);
+ },
+ referenceFunction: exponentialResult,
+ automationTime: renderQuantum / sampleRate,
+ thresholdSetTarget: 0,
+ thresholdRamp: 1.13249e-5
+ }).then(done);
+ });
+
+ audit.defineTask("delayed expo ramp", function (done) {
+ // Like "delayed linear ramp", but with an exponential ramp instead.
+ runTest("Delayed exponential ramp", {
+ automationFunction: function (audioparam, endValue, endTime) {
+ audioparam.exponentialRampToValueAtTime(endValue, endTime);
+ },
+ referenceFunction: exponentialResult,
+ automationTime: 4 * renderQuantum / sampleRate,
+ thresholdSetTarget: 2.19417e-7,
+ thresholdRamp: 4.17233e-6
+ }).then(done);
+ });
+
+ audit.defineTask("finish", function (done) {
+ finishJSTest();
+ done();
+ });
+
+ audit.runTasks();
+
+ function computeExpectedResult(automationTime, timeConstant, endValue, endTime, rampFunction) {
+ // The result is a constant value of 1 for one rendering quantum, then a SetTarget event
+ // lasting to |automationTime|, at which point a ramp starts which ends at |endValue|
+ // at |endTime|. Then the rest of curve should be held constant at |endValue|.
+ var initialPart = new Array(renderQuantum);
+ initialPart.fill(1);
+
+ // Generate 1 extra frame so that we know where to start the linear ramp. The last sample
+ // of the array is where the ramp should start from.
+ var setTargetPart = createExponentialApproachArray(renderQuantum / sampleRate,
+ automationTime + 1 / sampleRate, 1, 0, sampleRate, timeConstant);
+ var setTargetLength = setTargetPart.length;
+
+ // Generate the ramp starting at |automationTime| with a value from last value of the
+ // SetTarget curve above.
+ var rampPart = rampFunction(automationTime, endTime,
+ setTargetPart[setTargetLength - 1], endValue, sampleRate);
+
+ // Finally finish out the rest with a constant value of |endValue|, if needed.
+ var finalPart = new Array(Math.floor((renderDuration - endTime) * sampleRate));
+ finalPart.fill(endValue);
+
+ // Return the four parts separately for testing.
+ return {
+ initialPart: initialPart,
+ setTargetPart: setTargetPart.slice(0, setTargetLength - 1),
+ rampPart: rampPart,
+ tailPart: finalPart
+ };
+ }
+
+ function linearResult(automationTime, timeConstant, endValue, endTime) {
+ return computeExpectedResult(automationTime, timeConstant, endValue, endTime, createLinearRampArray);
+ }
+
+ function exponentialResult(automationTime, timeConstant, endValue, endTime) {
+ return computeExpectedResult(automationTime, timeConstant, endValue, endTime, createExponentialRampArray);
+ }
+
+ // Run test to verify that a SetTarget followed by a ramp produces a continuous curve.
+ // |prefix| is a string to use as a prefix for the messages. |options| is a dictionary
+ // describing how the test is run:
+ //
+ // |options.automationFunction|
+ // The function to use to start the automation, which should be a linear or exponential
+ // ramp automation. This function has three arguments:
+ // audioparam - the AudioParam to be automated
+ // endValue - the end value of the ramp
+ // endTime - the end time fo the ramp.
+ // |options.referenceFunction|
+ // The function to generated the expected result. This function has four arguments:
+ // automationTime - the value of |options.automationTime|
+ // timeConstant - time constant used for SetTargetAtTime
+ // rampEndValue - end value for the ramp (same value used for automationFunction)
+ // rampEndTime - end time for the ramp (same value used for automationFunction)
+ // |options.automationTime|
+ // Time at which the |automationFunction| is called to start the automation.
+ // |options.thresholdSetTarget|
+ // Threshold to use for verifying that the initial (if any) SetTargetAtTime portion had
+ // the correct values.
+ // |options.thresholdRamp|
+ // Threshold to use for verifying that the ramp portion had the correct values.
+ function runTest(prefix, options) {
+ var automationFunction = options.automationFunction;
+ var referenceFunction = options.referenceFunction;
+ var automationTime = options.automationTime;
+ var thresholdSetTarget = options.thresholdSetTarget || 0;
+ var thresholdRamp = options.thresholdRamp || 0;
+
+ // End value for the ramp. Fairly arbitrary, but should be distinctly different from the
+ // target value for SetTargetAtTime and the initial value of gain.gain.
+ var rampEndValue = 2;
+ var context = new OfflineAudioContext(1, renderFrames, sampleRate);
+
+ // A constant source of amplitude 1.
+ var source = context.createBufferSource();
+ source.buffer = createConstantBuffer(context, 1, 1);
+ source.loop = true;
+
+ var gain = context.createGain();
+
+ // The SetTarget starts after one rendering quantum.
+ gain.gain.setTargetAtTime(0, renderQuantum / context.sampleRate, timeConstant);
+
+ // Schedule the ramp at |automationTime|. If this time is past the first rendering quantum,
+ // the SetTarget event will run for a bit before running the ramp. Otherwise, the SetTarget
+ // should be completely replaced by the ramp.
+ context.suspend(automationTime)
+ .then(function () {
+ automationFunction(gain.gain, rampEndValue, rampEndTime);
+ context.resume();
+ });
+
+ source.connect(gain);
+ gain.connect(context.destination);
+
+ source.start();
+
+ return context.startRendering().then(function (resultBuffer) {
+ var success = true;
+ var result = resultBuffer.getChannelData(0);
+ var expected = referenceFunction(automationTime, timeConstant, rampEndValue, rampEndTime);
+
+ // Verify each part of the curve separately.
+ var startIndex = 0;
+ var length = expected.initialPart.length;
+
+ // Verify that the initial part of the curve is constant.
+ success = Should(prefix + ": Initial part", result.slice(0, length))
+ .beCloseToArray(expected.initialPart, 0) && success;
+
+ // Verify the SetTarget part of the curve, if the SetTarget did actually run.
+ startIndex += length;
+ length = expected.setTargetPart.length;
+ if (length) {
+ success = Should(prefix + ": SetTarget part", result.slice(startIndex, startIndex +
+ length))
+ .beCloseToArray(expected.setTargetPart, thresholdSetTarget) && success;
+ } else {
+ testPassed("SetTarget part was correctly replaced by the ramp");
+ }
+
+ // Verify the ramp part of the curve
+ startIndex += length;
+ length = expected.rampPart.length;
+ success = Should(prefix, result.slice(startIndex, startIndex + length), {
+ verbose: true
+ }).beCloseToArray(expected.rampPart, thresholdRamp) && success;
+
+ // Verify that the end of the curve after the ramp (if any) is a constant.
+ startIndex += length;
+ success = Should(prefix + ": Tail part", result.slice(startIndex))
+ .beCloseToArray(expected.tailPart, 0) && success;
+
+ if (success)
+ testPassed(prefix + " preceded by SetTarget is continuous.\n");
+ else
+ testFailed(prefix + " preceded by SetTarget was not continuous.\n");
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/third_party/WebKit/Source/modules/webaudio/AudioParamTimeline.cpp b/third_party/WebKit/Source/modules/webaudio/AudioParamTimeline.cpp
index dccad80e..54068c6 100644
--- a/third_party/WebKit/Source/modules/webaudio/AudioParamTimeline.cpp
+++ b/third_party/WebKit/Source/modules/webaudio/AudioParamTimeline.cpp
@@ -418,6 +418,46 @@
continue;
}
+ // If there's no next event, set nextEventType to LastType to indicate that.
+ ParamEvent::Type nextEventType = nextEvent ? static_cast<ParamEvent::Type>(nextEvent->getType()) : ParamEvent::LastType;
+
+ // If the current event is SetTarget and the next event is a LinearRampToValue or
+ // ExponentialRampToValue, special handling is needed. In this case, the linear and
+ // exponential ramp should start at wherever the SetTarget processing has reached.
+ if (event.getType() == ParamEvent::SetTarget
+ && (nextEventType == ParamEvent::LinearRampToValue
+ || nextEventType == ParamEvent::ExponentialRampToValue)) {
+ // Replace the SetTarget with a SetValue to set the starting time and value for the ramp
+ // using the current frame. We need to update |value| appropriately depending on
+ // whether the ramp has started or not.
+ //
+ // If SetTarget starts somewhere between currentFrame - 1 and currentFrame, we directly
+ // compute the value it would have at currentFrame. If not, we update the value from
+ // the value from currentFrame - 1.
+ //
+ // Can't use the condition currentFrame - 1 <= t0 * sampleRate <= currentFrame because
+ // currentFrame is unsigned and could be 0. Instead, compute the condition this way,
+ // where f = currentFrame and Fs = sampleRate:
+ //
+ // f - 1 <= t0 * Fs <= f
+ // 2 * f - 2 <= 2 * Fs * t0 <= 2 * f
+ // -2 <= 2 * Fs * t0 - 2 * f <= 0
+ // -1 <= 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) {
+ // SetTarget is starting somewhere between currentFrame - 1 and
+ // currentFrame. Compute the value the SetTarget would have at the currentFrame.
+ value = event.value() + (value - event.value()) * exp(-(currentFrame / sampleRate - event.time()) / event.timeConstant());
+ } else {
+ // SetTarget has already started. Update |value| one frame because it's the value from
+ // the previous frame.
+ float discreteTimeConstant = static_cast<float>(AudioUtilities::discreteTimeConstantForSampleRate(
+ event.timeConstant(), controlRate));
+ value += (event.value() - value) * discreteTimeConstant;
+ }
+ m_events[i] = ParamEvent::createSetValueEvent(value, currentFrame / sampleRate);
+ }
+
float value1 = event.value();
double time1 = event.time();
@@ -446,8 +486,6 @@
size_t fillToFrame = fillToEndFrame - startFrame;
fillToFrame = std::min(fillToFrame, static_cast<size_t>(numberOfValues));
- ParamEvent::Type nextEventType = nextEvent ? static_cast<ParamEvent::Type>(nextEvent->getType()) : ParamEvent::LastType /* unknown */;
-
// First handle linear and exponential ramps which require looking ahead to the next event.
if (nextEventType == ParamEvent::LinearRampToValue) {
const float valueDelta = value2 - value1;
@@ -534,6 +572,11 @@
// computed value.
if (writeIndex >= 1)
value /= multiplier;
+
+ // Due to roundoff it's possible that value exceeds value2. Clip value to value2 if
+ // we are within 1/2 frame of time2.
+ if (currentFrame > time2 * sampleRate - 0.5)
+ value = value2;
}
} else {
// Handle event types not requiring looking ahead to the next event.