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.