<!DOCTYPE html>
<html>
  <head>
    <title>
      Test Clamping of Distance for PannerNode
    </title>
    <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>
  </head>
  <body>
    <script id="layout-test-code">
      // Arbitrary sample rate and render length.
      let sampleRate = 48000;
      let renderFrames = 128;

      let audit = Audit.createTaskRunner();

      audit.define('ref-distance-error', (task, should) => {
        testDistanceLimits(should, {name: 'refDistance', isZeroAllowed: true});
        task.done();
      });

      audit.define('max-distance-error', (task, should) => {
        testDistanceLimits(should, {name: 'maxDistance', isZeroAllowed: false});
        task.done();
      });

      function testDistanceLimits(should, options) {
        // Verify that exceptions are thrown for invalid values of refDistance.
        let context = new OfflineAudioContext(1, renderFrames, sampleRate);

        let attrName = options.name;
        let prefix = 'new PannerNode(c, {' + attrName + ': ';

        should(function() {
          let nodeOptions = {};
          nodeOptions[attrName] = -1;
          new PannerNode(context, nodeOptions);
        }, prefix + '-1})').throw(RangeError);

        if (options.isZeroAllowed) {
          should(function() {
            let nodeOptions = {};
            nodeOptions[attrName] = 0;
            new PannerNode(context, nodeOptions);
          }, prefix + '0})').notThrow();
        } else {
          should(function() {
            let nodeOptions = {};
            nodeOptions[attrName] = 0;
            new PannerNode(context, nodeOptions);
          }, prefix + '0})').throw(RangeError);
        }

        // The smallest representable positive single float.
        let leastPositiveDoubleFloat = 4.9406564584124654e-324;

        should(function() {
          let nodeOptions = {};
          nodeOptions[attrName] = leastPositiveDoubleFloat;
          new PannerNode(context, nodeOptions);
        }, prefix + leastPositiveDoubleFloat + '})').notThrow();

        prefix = 'panner.' + attrName + ' = ';
        panner = new PannerNode(context);
        should(function() {
          panner[attrName] = -1;
        }, prefix + '-1').throw(RangeError);

        if (options.isZeroAllowed) {
          should(function() {
            panner[attrName] = 0;
          }, prefix + '0').notThrow();
        } else {
          should(function() {
            panner[attrName] = 0;
          }, prefix + '0').throw(RangeError);
        }

        should(function() {
          panner[attrName] = leastPositiveDoubleFloat;
        }, prefix + leastPositiveDoubleFloat).notThrow();
      }

      audit.define('min-distance', async (task, should) => {
        // Test clamping of panner distance to refDistance for all of the
        // distance models.  The actual distance is arbitrary as long as it's
        // less than refDistance.  We test default and non-default values for
        // the panner's refDistance and maxDistance.
        // correctly.
        await runTest(should, {
          distance: 0.01,
          distanceModel: 'linear',
        });
        await runTest(should, {
          distance: 0.01,
          distanceModel: 'exponential',
        });
        await runTest(should, {
          distance: 0.01,
          distanceModel: 'inverse',
        });
        await runTest(should, {
          distance: 2,
          distanceModel: 'linear',
          maxDistance: 1000,
          refDistance: 10,
        });
        await runTest(should, {
          distance: 2,
          distanceModel: 'exponential',
          maxDistance: 1000,
          refDistance: 10,
        });
        await runTest(should, {
          distance: 2,
          distanceModel: 'inverse',
          maxDistance: 1000,
          refDistance: 10,
        });
        task.done();
      });

      audit.define('max-distance', async (task, should) => {
        // Like the "min-distance" task, but for clamping to the max
        // distance. The actual distance is again arbitrary as long as it is
        // greater than maxDistance.
        await runTest(should, {
          distance: 20000,
          distanceModel: 'linear',
        });
        await runTest(should, {
          distance: 21000,
          distanceModel: 'exponential',
        });
        await runTest(should, {
          distance: 23000,
          distanceModel: 'inverse',
        });
        await runTest(should, {
          distance: 5000,
          distanceModel: 'linear',
          maxDistance: 1000,
          refDistance: 10,
        });
        await runTest(should, {
          distance: 5000,
          distanceModel: 'exponential',
          maxDistance: 1000,
          refDistance: 10,
        });
        await runTest(should, {
          distance: 5000,
          distanceModel: 'inverse',
          maxDistance: 1000,
          refDistance: 10,
        });
        task.done();
      });

      function runTest(should, options) {
        let context = new OfflineAudioContext(2, renderFrames, sampleRate);
        let src = new OscillatorNode(context, {
          type: 'sawtooth',
          frequency: 20 * 440,
        });

        // Set panner options.  Use a non-default rolloffFactor so that the
        // various distance models look distinctly different.
        let pannerOptions = {};
        Object.assign(pannerOptions, options, {rolloffFactor: 0.5});

        let pannerRef = new PannerNode(context, pannerOptions);
        let pannerTest = new PannerNode(context, pannerOptions);

        // Split the panner output so we can grab just one of the output
        // channels.
        let splitRef = new ChannelSplitterNode(context, {numberOfOutputs: 2});
        let splitTest = new ChannelSplitterNode(context, {numberOfOutputs: 2});

        // Merge the panner outputs back into one stereo stream for the
        // destination.
        let merger = new ChannelMergerNode(context, {numberOfInputs: 2});

        src.connect(pannerTest).connect(splitTest).connect(merger, 0, 0);
        src.connect(pannerRef).connect(splitRef).connect(merger, 0, 1);

        merger.connect(context.destination);

        // Move the panner some distance away. Arbitrarily select the x
        // direction.  For the reference panner, manually clamp the distance.
        // All models clamp the distance to a minimum of refDistance.  Only the
        // linear model also clamps to a maximum of maxDistance.
        let xRef = Math.max(options.distance, pannerRef.refDistance);

        if (pannerRef.distanceModel === 'linear') {
          xRef = Math.min(xRef, pannerRef.maxDistance);
        }

        let xTest = options.distance;

        pannerRef.positionZ.setValueAtTime(xRef, 0);
        pannerTest.positionZ.setValueAtTime(xTest, 0);

        src.start();

        return context.startRendering().then(function(resultBuffer) {
          let actual = resultBuffer.getChannelData(0);
          let expected = resultBuffer.getChannelData(1);

          should(
              xTest < pannerRef.refDistance || xTest > pannerRef.maxDistance,
              'Model: ' + options.distanceModel + ': Distance (' + xTest +
                  ') is outside the range [' + pannerRef.refDistance + ', ' +
                  pannerRef.maxDistance + ']')
              .beEqualTo(true);
          should(actual, 'Test panner output ' + JSON.stringify(options))
              .beEqualToArray(expected);
        });
      }

      audit.run();
    </script>
  </body>
</html>
