<!DOCTYPE html>
<html>
  <head>
    <title>
      Test CancelValuesAndHoldAtTime
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audio-param.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let sampleRate = 48000;
      let renderDuration = 0.5;

      let audit = Audit.createTaskRunner();

      audit.define(
          {label: 'cancelTime', description: 'Test Invalid Values'},
          (task, should) => {
            let context = new OfflineAudioContext({
              numberOfChannels: 1,
              length: 1,
              sampleRate: 8000
            });

            let src = new ConstantSourceNode(context);
            src.connect(context.destination);

            should(
                () => src.offset.cancelAndHoldAtTime(-1),
                'cancelAndHoldAtTime(-1)')
                .throw(RangeError);

            // These are TypeErrors because |cancelTime| is a
            // double, not unrestricted double.
            should(
                () => src.offset.cancelAndHoldAtTime(NaN),
                'cancelAndHoldAtTime(NaN)')
                .throw(TypeError);

            should(
                () => src.offset.cancelAndHoldAtTime(Infinity),
                'cancelAndHoldAtTime(Infinity)')
                .throw(TypeError);

            task.done();
          });

      // 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(
          {label: 'linear', description: 'Cancel linearRampToValueAtTime'},
          function(task, should) {
            cancelTest(should, linearRampTest('linearRampToValueAtTime'), {
              valueThreshold: 8.3998e-5,
              curveThreshold: 5.9605e-5
            }).then(task.done.bind(task));
          });

      audit.define(
          {label: 'exponential', description: 'Cancel exponentialRampAtTime'},
          function(task, should) {
            // 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(
          {label: 'setTarget', description: 'Cancel setTargetAtTime'},
          function(task, should) {
            // 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(
          {label: 'setValueCurve', description: 'Cancel setValueCurveAtTime'},
          function(task, should) {
            // 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(
          {
            label: 'setValueCurve after end',
            description: 'Cancel setValueCurveAtTime after the end'
          },
          function(task, should) {
            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(
          {
            label: 'initial setTarget',
            description: 'Cancel with initial setTargetAtTime'
          },
          function(task, should) {
            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: 3.1210e-6,
              curveThreshold: 0
            }).then(task.done.bind(task));
          });

      // Test automations scheduled after the call to cancelAndHoldAtTime.
      // Very similar to the above tests, but we also schedule an event after
      // cancelAndHoldAtTime and verify that curve after cancellation has
      // the correct values.

      audit.define(
          {
            label: 'post cancel: Linear',
            description: 'LinearRamp after cancelling'
          },
          function(task, should) {
            // Run the cancel test using a linearRamp as the event to be
            // cancelled. Then schedule another linear ramp after the
            // cancellation.
            cancelTest(
                should,
                linearRampTest('Post cancellation linearRampToValueAtTime'),
                {valueThreshold: 8.3998e-5, curveThreshold: 5.9605e-8},
                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(
          {
            label: 'post cancel: Exponential',
            description: 'ExponentialRamp after cancelling'
          },
          function(task, should) {
            // 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: 5.9605e-8},
                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: 5.9605e-8},
            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: 5.9605e-8},
            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: 5.9605e-8},
            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.define('cancel future setTarget', (task, should) => {
        const context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
        const src = new ConstantSourceNode(context);
        src.connect(context.destination);

        src.offset.setValueAtTime(0.5, 0);
        src.offset.setTargetAtTime(0, 0.75 * renderDuration, 0.1);
        // Now cancel the effect of the setTarget.
        src.offset.cancelAndHoldAtTime(0.5 * renderDuration);

        src.start();
        context.startRendering()
            .then(buffer => {
              let actual = buffer.getChannelData(0);
              // Because the setTarget was cancelled, the output should be a
              // constant.
              should(actual, 'After cancelling future setTarget event, output')
                  .beConstantValueOf(0.5);
            })
            .then(task.done.bind(task));
      });

      audit.define('cancel setTarget now', (task, should) => {
        const context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
        const src = new ConstantSourceNode(context);
        src.connect(context.destination);

        src.offset.setValueAtTime(0.5, 0);
        src.offset.setTargetAtTime(0, 0.5 * renderDuration, 0.1);
        // Now cancel the effect of the setTarget.
        src.offset.cancelAndHoldAtTime(0.5 * renderDuration);

        src.start();
        context.startRendering()
            .then(buffer => {
              let actual = buffer.getChannelData(0);
              // Because the setTarget was cancelled, the output should be a
              // constant.
              should(
                  actual,
                  'After cancelling setTarget event starting now, output')
                  .beConstantValueOf(0.5);
            })
            .then(task.done.bind(task));
      });

      audit.define('cancel future setValueCurve', (task, should) => {
        const context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
        const src = new ConstantSourceNode(context);
        src.connect(context.destination);

        src.offset.setValueAtTime(0.5, 0);
        src.offset.setValueCurveAtTime([-1, 1], 0.75 * renderDuration, 0.1);
        // Now cancel the effect of the setTarget.
        src.offset.cancelAndHoldAtTime(0.5 * renderDuration);

        src.start();
        context.startRendering()
            .then(buffer => {
              let actual = buffer.getChannelData(0);
              // Because the setTarget was cancelled, the output should be a
              // constant.
              should(
                  actual, 'After cancelling future setValueCurve event, output')
                  .beConstantValueOf(0.5);
            })
            .then(task.done.bind(task));
      });

      audit.define('cancel setValueCurve now', (task, should) => {
        const context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
        const src = new ConstantSourceNode(context);
        src.connect(context.destination);

        src.offset.setValueAtTime(0.5, 0);
        src.offset.setValueCurveAtTime([-1, 1], 0.5 * renderDuration, 0.1);
        // Now cancel the effect of the setTarget.
        src.offset.cancelAndHoldAtTime(0.5 * renderDuration);

        src.start();
        context.startRendering()
            .then(buffer => {
              let actual = buffer.getChannelData(0);
              // Because the setTarget was cancelled, the output should be a
              // constant.
              should(
                  actual,
                  'After cancelling current setValueCurve event starting now, output')
                  .beConstantValueOf(0.5);
            })
            .then(task.done.bind(task));
      });

      audit.define(
        {
          label: 'linear, cancel, linear, cancel, linear',
          description: 'Schedules 3 linear ramps, cancelling 2 of them, '
            + 'so that we end up with 2 cancel events next to each other'
        },
        (task, should) => {
          cancelTest2(
            should,
            linearRampTest('1st linearRamp'),
            {valueThreshold: 0, curveThreshold: 5.9605e-8},
            (g, cancelTime, expectedConstant, cancelTime2) => {
              // Ramp from first cancel time to the end will be cancelled at
              // second cancel time.
              const v1 = expectedConstant;
              const t1 = cancelTime;
              const v2 = 2;
              const t2 = renderDuration;
              g[0].gain.linearRampToValueAtTime(v2, t2);
              g[2].gain.setValueAtTime(v1, t1);
              g[2].gain.linearRampToValueAtTime(v2, t2);

              const expectedConstant2 =
                audioParamLinearRamp(cancelTime2, v1, t1, v2, t2);

              return {
                constantEndTime: cancelTime,
                message: `2nd linearRamp(${v2}, ${t2})`,
                expectedConstant2
              };
            },
            (g, cancelTime2, expectedConstant2) => {
              // Ramp from second cancel time to the end.
              const v3 = 0;
              const t3 = renderDuration;
              g[0].gain.linearRampToValueAtTime(v3, t3);
              g[3].gain.setValueAtTime(expectedConstant2, cancelTime2);
              g[3].gain.linearRampToValueAtTime(v3, t3);
              return {
                constantEndTime2: cancelTime2,
                message2: `3rd linearRamp(${v3}, ${t3})`,
              };
            })
            .then(() => task.done());
        });

      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:
                message + ': linearRampToValue(' + v1 + ', ' + t1 + ')',
            summary: message,
          };
        }
      }

      // Run the cancellation test. A set of automations is created and
      // canceled.
      //
      // |testerFunction| 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 cancelAndHoldAtTime 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.cancelAndHoldAtTime(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 cancelAndHoldAtTime) and
        // channel 1 is the reference (without cancelAndHoldAtTime).
        // 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});
          }
        });
      }

      // Similar to cancelTest, but does 2 cancels.
      function cancelTest2(
          should, testerFunction, thresholdOptions,
          postCancelTest, postCancelTest2) {
        // Channel 0: Actual output that includes the cancellation of events.
        // Channel 1: Expected data up to the first cancellation.
        // Channel 2: Expected data from 1st cancellation to 2nd cancellation.
        // Channel 3: Expected data from 2nd cancellation to the end.
        const context =
          new OfflineAudioContext(4, renderDuration * sampleRate, sampleRate);

        const src = context.createConstantSource();

        // g0: Actual gain which will have cancelAndHoldAtTime called on it
        // twice.
        // g1: Expected gain from start to the 1st cancel.
        // g2: Expected gain from 1st cancel to the 2nd cancel.
        // g3: Expected gain from the 2nd cancel to the end.
        const g0 = context.createGain();
        const g1 = context.createGain();
        const g2 = context.createGain();
        const g3 = context.createGain();
        const v0 = 1;
        const t0 = 0.01;

        const cancelTime1 = renderDuration * 0.5;
        const cancelTime2 = renderDuration * 0.75;

        // Run testerFunction to generate the 1st ramp.
        const {
          expectedConstant, autoMessage, summaryMessage} =
            testerFunction([g0, g1, g2], v0, t0, cancelTime1);

        // 1st cancel, cancelling the 1st ramp.
        g0.gain.cancelAndHoldAtTime(cancelTime1);

        // Run postCancelTest to generate the 2nd ramp.
        const {
          constantEndTime, message, errorThreshold = 0, expectedConstant2} =
            postCancelTest(
              [g0, g1, g2], cancelTime1, expectedConstant, cancelTime2);

        // 2nd cancel, cancelling the 2nd ramp.
        g0.gain.cancelAndHoldAtTime(cancelTime2);

        // Run postCancelTest2 to generate the 3rd ramp.
        const {constantEndTime2, message2} =
          postCancelTest2([g0, g1, g2, g3], cancelTime2, expectedConstant2);

        // Connect everything together
        src.connect(g0);
        src.connect(g1);
        src.connect(g2);
        src.connect(g3);
        const merger = context.createChannelMerger(4);
        g0.connect(merger, 0, 0);
        g1.connect(merger, 0, 1);
        g2.connect(merger, 0, 2);
        g3.connect(merger, 0, 3);
        merger.connect(context.destination);

        // Go!
        src.start();

        return context.startRendering().then(function (buffer) {
          const actual = buffer.getChannelData(0);
          const expected1 = buffer.getChannelData(1);
          const expected2 = buffer.getChannelData(2);
          const expected3 = buffer.getChannelData(3);

          const cancelFrame1 = Math.ceil(cancelTime1 * sampleRate);
          const cancelFrame2 = Math.ceil(cancelTime2 * sampleRate);

          const constantEndFrame1 = Math.ceil(constantEndTime * sampleRate);
          const constantEndFrame2 = Math.ceil(constantEndTime2 * sampleRate);

          const actualTail1 = actual.slice(cancelFrame1, constantEndFrame1);
          const actualTail2 = actual.slice(cancelFrame2, constantEndFrame2);

          const actualConstant1 = actual[cancelFrame1];
          const actualConstant2 = actual[cancelFrame2];

          // Verify first section curve
          should(
            actual.slice(0, cancelFrame1),
            autoMessage + ' up to time ' + cancelTime1)
            .beCloseToArray(
              expected1.slice(0, cancelFrame1),
              {absoluteThreshold: thresholdOptions.curveThreshold});

          // Verify that a value was held after 1st cancel
          should(
            actualTail1,
            'Cancelling ' + autoMessage + ' at time ' + cancelTime1)
            .beConstantValueOf(actualConstant1);

          // Verify that held value after 1st cancel was correct
          should(
            actualConstant1,
            'Expected value for cancelling ' + autoMessage + ' at time ' +
            cancelTime1)
            .beCloseTo(
              expectedConstant,
              {threshold: thresholdOptions.valueThreshold});

          // Verify middle section curve
          should(actual.slice(constantEndFrame1, cancelFrame2), message)
            .beCloseToArray(
              expected2.slice(constantEndFrame1, cancelFrame2),
              {absoluteThreshold: errorThreshold});

          // Verify that a value was held after 2nd cancel
          should(
            actualTail2,
            'Cancelling ' + message + ' at time ' + cancelTime2)
            .beConstantValueOf(actualConstant2);

          // Verify that held value after 2nd cancel was correct
          should(
            actualConstant2,
            'Expected value for cancelling ' + message + ' at time ' +
            cancelTime2)
            .beCloseTo(
              expectedConstant2,
              {threshold: thresholdOptions.valueThreshold});

          // Verify end section curve
          should(actual.slice(constantEndFrame2), message2)
            .beCloseToArray(
              expected3.slice(constantEndFrame2),
              {absoluteThreshold: errorThreshold || 0});
        });
      }
    </script>
  </body>
</html>
