<!DOCTYPE html>
<html>
  <head>
    <title>
      audionode-channel-rules.html
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
    <script src="/webaudio/resources/mixing-rules.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let audit = Audit.createTaskRunner();
      let context = 0;
      // Use a power of two to eliminate round-off converting frames to time.
      let sampleRate = 32768;
      let renderNumberOfChannels = 8;
      let singleTestFrameLength = 8;
      let testBuffers;

      // A list of connections to an AudioNode input, each of which is to be
      // used in one or more specific test cases.  Each element in the list is a
      // string, with the number of connections corresponding to the length of
      // the string, and each character in the string is from '1' to '8'
      // representing a 1 to 8 channel connection (from an AudioNode output).

      // For example, the string "128" means 3 connections, having 1, 2, and 8
      // channels respectively.

      let connectionsList = [
        '1', '2', '3', '4', '5', '6', '7', '8', '11', '12', '14', '18', '111',
        '122', '123', '124', '128'
      ];

      // A list of mixing rules, each of which will be tested against all of the
      // connections in connectionsList.
      let mixingRulesList = [
        {
          channelCount: 2,
          channelCountMode: 'max',
          channelInterpretation: 'speakers'
        },
        {
          channelCount: 4,
          channelCountMode: 'clamped-max',
          channelInterpretation: 'speakers'
        },

        // Test up-down-mix to some explicit speaker layouts.
        {
          channelCount: 1,
          channelCountMode: 'explicit',
          channelInterpretation: 'speakers'
        },
        {
          channelCount: 2,
          channelCountMode: 'explicit',
          channelInterpretation: 'speakers'
        },
        {
          channelCount: 4,
          channelCountMode: 'explicit',
          channelInterpretation: 'speakers'
        },
        {
          channelCount: 6,
          channelCountMode: 'explicit',
          channelInterpretation: 'speakers'
        },

        {
          channelCount: 2,
          channelCountMode: 'max',
          channelInterpretation: 'discrete'
        },
        {
          channelCount: 4,
          channelCountMode: 'clamped-max',
          channelInterpretation: 'discrete'
        },
        {
          channelCount: 4,
          channelCountMode: 'explicit',
          channelInterpretation: 'discrete'
        },
        {
          channelCount: 8,
          channelCountMode: 'explicit',
          channelInterpretation: 'discrete'
        },
      ];

      let numberOfTests = mixingRulesList.length * connectionsList.length;

      // Print out the information for an individual test case.
      function printTestInformation(
          testNumber, actualBuffer, expectedBuffer, frameLength, frameOffset) {
        let actual = stringifyBuffer(actualBuffer, frameLength);
        let expected =
            stringifyBuffer(expectedBuffer, frameLength, frameOffset);
        debug('TEST CASE #' + testNumber + '\n');
        debug('actual channels:\n' + actual);
        debug('expected channels:\n' + expected);
      }

      function scheduleTest(
          testNumber, connections, channelCount, channelCountMode,
          channelInterpretation) {
        let mixNode = context.createGain();
        mixNode.channelCount = channelCount;
        mixNode.channelCountMode = channelCountMode;
        mixNode.channelInterpretation = channelInterpretation;
        mixNode.connect(context.destination);

        for (let i = 0; i < connections.length; ++i) {
          let connectionNumberOfChannels =
              connections.charCodeAt(i) - '0'.charCodeAt(0);

          let source = context.createBufferSource();
          // Get a buffer with the right number of channels, converting from
          // 1-based to 0-based index.
          let buffer = testBuffers[connectionNumberOfChannels - 1];
          source.buffer = buffer;
          source.connect(mixNode);

          // Start at the right offset.
          let sampleFrameOffset = testNumber * singleTestFrameLength;
          let time = sampleFrameOffset / sampleRate;
          source.start(time);
        }
      }

      function checkTestResult(
          renderedBuffer, testNumber, connections, channelCount,
          channelCountMode, channelInterpretation, should) {
        let s = 'connections: ' + connections + ', ' + channelCountMode;

        // channelCount is ignored in "max" mode.
        if (channelCountMode == 'clamped-max' ||
            channelCountMode == 'explicit') {
          s += '(' + channelCount + ')';
        }

        s += ', ' + channelInterpretation;

        let computedNumberOfChannels = computeNumberOfChannels(
            connections, channelCount, channelCountMode);

        // Create a zero-initialized silent AudioBuffer with
        // computedNumberOfChannels.
        let destBuffer = context.createBuffer(
            computedNumberOfChannels, singleTestFrameLength,
            context.sampleRate);

        // Mix all of the connections into the destination buffer.
        for (let i = 0; i < connections.length; ++i) {
          let connectionNumberOfChannels =
              connections.charCodeAt(i) - '0'.charCodeAt(0);
          let sourceBuffer =
              testBuffers[connectionNumberOfChannels - 1];  // convert from
                                                            // 1-based to
                                                            // 0-based index

          if (channelInterpretation == 'speakers') {
            speakersSum(sourceBuffer, destBuffer);
          } else if (channelInterpretation == 'discrete') {
            discreteSum(sourceBuffer, destBuffer);
          } else {
            alert('Invalid channel interpretation!');
          }
        }

        // Use this when debugging mixing rules.
        // printTestInformation(testNumber, renderedBuffer, destBuffer,
        // singleTestFrameLength, sampleFrameOffset);

        // Validate that destBuffer matches the rendered output.  We need to
        // check the rendered output at a specific sample-frame-offset
        // corresponding to the specific test case we're checking for based on
        // testNumber.

        let sampleFrameOffset = testNumber * singleTestFrameLength;
        for (let c = 0; c < renderNumberOfChannels; ++c) {
          let renderedData = renderedBuffer.getChannelData(c);
          for (let frame = 0; frame < singleTestFrameLength; ++frame) {
            let renderedValue = renderedData[frame + sampleFrameOffset];

            let expectedValue = 0;
            if (c < destBuffer.numberOfChannels) {
              let expectedData = destBuffer.getChannelData(c);
              expectedValue = expectedData[frame];
            }

            // We may need to add an epsilon in the comparison if we add more
            // test vectors.
            if (renderedValue != expectedValue) {
              let message = s + 'rendered: ' + renderedValue +
                  ' expected: ' + expectedValue + ' channel: ' + c +
                  ' frame: ' + frame;
              // testFailed(s);
              should(renderedValue, s).beEqualTo(expectedValue);
              return;
            }
          }
        }

        should(true, s).beTrue();
      }

      function checkResult(buffer, should) {
        // Sanity check result.
        should(buffer.length, 'Rendered number of frames')
            .beEqualTo(numberOfTests * singleTestFrameLength);
        should(buffer.numberOfChannels, 'Rendered number of channels')
            .beEqualTo(renderNumberOfChannels);

        // Check all the tests.
        let testNumber = 0;
        for (let m = 0; m < mixingRulesList.length; ++m) {
          let mixingRules = mixingRulesList[m];
          for (let i = 0; i < connectionsList.length; ++i, ++testNumber) {
            checkTestResult(
                buffer, testNumber, connectionsList[i],
                mixingRules.channelCount, mixingRules.channelCountMode,
                mixingRules.channelInterpretation, should);
          }
        }
      }

      audit.define(
          {label: 'test', description: 'Channel mixing rules for AudioNodes'},
          function(task, should) {

            // Create 8-channel offline audio context.  Each test will render 8
            // sample-frames starting at sample-frame position testNumber * 8.
            let totalFrameLength = numberOfTests * singleTestFrameLength;
            context = new OfflineAudioContext(
                renderNumberOfChannels, totalFrameLength, sampleRate);

            // Set destination to discrete mixing.
            context.destination.channelCount = renderNumberOfChannels;
            context.destination.channelCountMode = 'explicit';
            context.destination.channelInterpretation = 'discrete';

            // Create test buffers from 1 to 8 channels.
            testBuffers = new Array();
            for (let i = 0; i < renderNumberOfChannels; ++i) {
              testBuffers[i] = createShiftedImpulseBuffer(
                  context, i + 1, singleTestFrameLength);
            }

            // Schedule all the tests.
            let testNumber = 0;
            for (let m = 0; m < mixingRulesList.length; ++m) {
              let mixingRules = mixingRulesList[m];
              for (let i = 0; i < connectionsList.length; ++i, ++testNumber) {
                scheduleTest(
                    testNumber, connectionsList[i], mixingRules.channelCount,
                    mixingRules.channelCountMode,
                    mixingRules.channelInterpretation);
              }
            }

            // Render then check results.
            // context.oncomplete = checkResult;
            context.startRendering().then(buffer => {
              checkResult(buffer, should);
              task.done();
            });
            ;
          });

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