Bug 865234 - Part 8: Add a test case for channel mixing rules; r=roc
authorEhsan Akhgari <ehsan@mozilla.com>
Tue, 30 Apr 2013 00:06:04 -0400
changeset 141405 ccdea6d194e9f47ed9debbd92ac9a000c341840b
parent 141404 7ed8524e54f5c3d740780d52cc73510ae6e80337
child 141406 7d8576824d5e845d624a58d8eef4bbaa99155ae0
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc
bugs865234
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 865234 - Part 8: Add a test case for channel mixing rules; r=roc
content/media/webaudio/test/Makefile.in
content/media/webaudio/test/test_mixingRules.html
--- a/content/media/webaudio/test/Makefile.in
+++ b/content/media/webaudio/test/Makefile.in
@@ -33,16 +33,17 @@ MOCHITEST_FILES := \
   test_badConnect.html \
   test_biquadFilterNode.html \
   test_currentTime.html \
   test_delayNode.html \
   test_delayNodeWithGain.html \
   test_decodeAudioData.html \
   test_dynamicsCompressorNode.html \
   test_gainNode.html \
+  test_mixingRules.html \
   test_pannerNode.html \
   test_scriptProcessorNode.html \
   test_singleSourceDest.html \
   test_audioBufferSourceNodeLazyLoopParam.html \
   ting.ogg \
   ting-expected.wav \
   ting-dualchannel44.1.ogg \
   ting-dualchannel44.1-expected.wav \
new file mode 100644
--- /dev/null
+++ b/content/media/webaudio/test/test_mixingRules.html
@@ -0,0 +1,403 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Testcase for AudioNode channel up-mix/down-mix rules</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="webaudio.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script>
+
+// This test is based on http://src.chromium.org/viewvc/blink/trunk/LayoutTests/webaudio/audionode-channel-rules.html
+
+var context = null;
+var sp = null;
+var renderNumberOfChannels = 8;
+var singleTestFrameLength = 8;
+var 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.
+var connectionsList = [];
+for (var i = 1; i <= 8; ++i) {
+  connectionsList.push(i.toString());
+  for (var j = 1; j <= 8; ++j) {
+    connectionsList.push(i.toString() + j.toString());
+  }
+}
+
+// A list of mixing rules, each of which will be tested against all of the connections in connectionsList.
+var mixingRulesList = [
+    {channelCount: 1, channelCountMode: "max", channelInterpretation: "speakers"},
+    {channelCount: 2, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+    {channelCount: 3, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+    {channelCount: 4, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+    {channelCount: 5, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+    {channelCount: 6, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+    {channelCount: 7, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+    {channelCount: 2, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 3, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 4, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 5, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 6, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 7, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 8, channelCountMode: "explicit", channelInterpretation: "speakers"},
+    {channelCount: 1, channelCountMode: "max", channelInterpretation: "discrete"},
+    {channelCount: 2, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+    {channelCount: 3, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+    {channelCount: 4, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+    {channelCount: 5, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+    {channelCount: 6, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+    {channelCount: 3, channelCountMode: "explicit", channelInterpretation: "discrete"},
+    {channelCount: 4, channelCountMode: "explicit", channelInterpretation: "discrete"},
+    {channelCount: 5, channelCountMode: "explicit", channelInterpretation: "discrete"},
+    {channelCount: 6, channelCountMode: "explicit", channelInterpretation: "discrete"},
+    {channelCount: 7, channelCountMode: "explicit", channelInterpretation: "discrete"},
+    {channelCount: 8, channelCountMode: "explicit", channelInterpretation: "discrete"},
+];
+
+var numberOfTests = mixingRulesList.length * connectionsList.length;
+
+// Create an n-channel buffer, with all sample data zero except for a shifted impulse.
+// The impulse position depends on the channel index.
+// For example, for a 4-channel buffer:
+// channel0: 1 0 0 0 0 0 0 0
+// channel1: 0 1 0 0 0 0 0 0
+// channel2: 0 0 1 0 0 0 0 0
+// channel3: 0 0 0 1 0 0 0 0
+function createTestBuffer(numberOfChannels) {
+    var buffer = context.createBuffer(numberOfChannels, singleTestFrameLength, context.sampleRate);
+    for (var i = 0; i < numberOfChannels; ++i) {
+        var data = buffer.getChannelData(i);
+        data[i] = 1;
+    }
+    return buffer;
+}
+
+// Discrete channel interpretation mixing:
+// https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix
+// up-mix by filling channels until they run out then ignore remaining dest channels.
+// down-mix by filling as many channels as possible, then dropping remaining source channels.
+function discreteSum(sourceBuffer, destBuffer) {
+    if (sourceBuffer.length != destBuffer.length) {
+        is(sourceBuffer.length, destBuffer.length, "source and destination buffers should have the same length");
+    }
+
+    var numberOfChannels = Math.min(sourceBuffer.numberOfChannels, destBuffer.numberOfChannels);
+    var length = sourceBuffer.length;
+
+    for (var c = 0; c < numberOfChannels; ++c) {
+        var source = sourceBuffer.getChannelData(c);
+        var dest = destBuffer.getChannelData(c);
+        for (var i = 0; i < length; ++i) {
+            dest[i] += source[i];
+        }
+    }
+}
+
+// Speaker channel interpretation mixing:
+// https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix
+function speakersSum(sourceBuffer, destBuffer)
+{
+    var numberOfSourceChannels = sourceBuffer.numberOfChannels;
+    var numberOfDestinationChannels = destBuffer.numberOfChannels;
+    var length = destBuffer.length;
+
+    if ((numberOfDestinationChannels == 2 && numberOfSourceChannels == 1) ||
+        (numberOfDestinationChannels == 4 && numberOfSourceChannels == 1)) {
+        // Handle mono -> stereo/Quad case (summing mono channel into both left and right).
+        var source = sourceBuffer.getChannelData(0);
+        var destL = destBuffer.getChannelData(0);
+        var destR = destBuffer.getChannelData(1);
+
+        for (var i = 0; i < length; ++i) {
+            destL[i] += source[i];
+            destR[i] += source[i];
+        }
+        } else if ((numberOfDestinationChannels == 4 && numberOfSourceChannels == 2) ||
+                   (numberOfDestinationChannels == 6 && numberOfSourceChannels == 2)) {
+        // Handle stereo -> Quad/5.1 case (summing left and right channels into the output's left and right).
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var destL = destBuffer.getChannelData(0);
+        var destR = destBuffer.getChannelData(1);
+
+        for (var i = 0; i < length; ++i) {
+            destL[i] += sourceL[i];
+            destR[i] += sourceR[i];
+        }
+    } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 2) {
+        // Handle stereo -> mono case. output += 0.5 * (input.L + input.R).
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var dest = destBuffer.getChannelData(0);
+
+        for (var i = 0; i < length; ++i) {
+            dest[i] += 0.5 * (sourceL[i] + sourceR[i]);
+        }
+    } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 4) {
+        // Handle Quad -> mono case. output += 0.25 * (input.L + input.R + input.SL + input.SR).
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var sourceSL = sourceBuffer.getChannelData(2);
+        var sourceSR = sourceBuffer.getChannelData(3);
+        var dest = destBuffer.getChannelData(0);
+
+        for (var i = 0; i < length; ++i) {
+            dest[i] += 0.25 * (sourceL[i] + sourceR[i] + sourceSL[i] + sourceSR[i]);
+        }
+    } else if (numberOfDestinationChannels == 2 && numberOfSourceChannels == 4) {
+        // Handle Quad -> stereo case. outputLeft += 0.5 * (input.L + input.SL),
+        //                             outputRight += 0.5 * (input.R + input.SR).
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var sourceSL = sourceBuffer.getChannelData(2);
+        var sourceSR = sourceBuffer.getChannelData(3);
+        var destL = destBuffer.getChannelData(0);
+        var destR = destBuffer.getChannelData(1);
+
+        for (var i = 0; i < length; ++i) {
+            destL[i] += 0.5 * (sourceL[i] + sourceSL[i]);
+            destR[i] += 0.5 * (sourceR[i] + sourceSR[i]);
+        }
+    } else if (numberOfDestinationChannels == 6 && numberOfSourceChannels == 4) {
+        // Handle Quad -> 5.1 case. outputLeft += (inputL, inputR, 0, 0, inputSL, inputSR)
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var sourceSL = sourceBuffer.getChannelData(2);
+        var sourceSR = sourceBuffer.getChannelData(3);
+        var destL = destBuffer.getChannelData(0);
+        var destR = destBuffer.getChannelData(1);
+        var destSL = destBuffer.getChannelData(4);
+        var destSR = destBuffer.getChannelData(5);
+
+        for (var i = 0; i < length; ++i) {
+            destL[i] += sourceL[i];
+            destR[i] += sourceR[i];
+            destSL[i] += sourceSL[i];
+            destSR[i] += sourceSR[i];
+        }
+    } else if (numberOfDestinationChannels == 6 && numberOfSourceChannels == 1) {
+        // Handle mono -> 5.1 case, sum mono channel into center.
+        var source = sourceBuffer.getChannelData(0);
+        var dest = destBuffer.getChannelData(2);
+
+        for (var i = 0; i < length; ++i) {
+            dest[i] += source[i];
+        }
+    } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 6) {
+        // Handle 5.1 -> mono.
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var sourceC = sourceBuffer.getChannelData(2);
+        // skip LFE for now, according to current spec.
+        var sourceSL = sourceBuffer.getChannelData(4);
+        var sourceSR = sourceBuffer.getChannelData(5);
+        var dest = destBuffer.getChannelData(0);
+
+        for (var i = 0; i < length; ++i) {
+            dest[i] += 0.7071 * (sourceL[i] + sourceR[i]) + sourceC[i] + 0.5 * (sourceSL[i] + sourceSR[i]);
+        }
+    } else if (numberOfDestinationChannels == 2 && numberOfSourceChannels == 6) {
+        // Handle 5.1 -> stereo.
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var sourceC = sourceBuffer.getChannelData(2);
+        // skip LFE for now, according to current spec.
+        var sourceSL = sourceBuffer.getChannelData(4);
+        var sourceSR = sourceBuffer.getChannelData(5);
+        var destL = destBuffer.getChannelData(0);
+        var destR = destBuffer.getChannelData(1);
+
+        for (var i = 0; i < length; ++i) {
+            destL[i] += sourceL[i] + 0.7071 * (sourceC[i] + sourceSL[i]);
+            destR[i] += sourceR[i] + 0.7071 * (sourceC[i] + sourceSR[i]);
+        }
+    } else if (numberOfDestinationChannels == 4 && numberOfSourceChannels == 6) {
+        // Handle 5.1 -> Quad.
+        var sourceL = sourceBuffer.getChannelData(0);
+        var sourceR = sourceBuffer.getChannelData(1);
+        var sourceC = sourceBuffer.getChannelData(2);
+        // skip LFE for now, according to current spec.
+        var sourceSL = sourceBuffer.getChannelData(4);
+        var sourceSR = sourceBuffer.getChannelData(5);
+        var destL = destBuffer.getChannelData(0);
+        var destR = destBuffer.getChannelData(1);
+        var destSL = destBuffer.getChannelData(2);
+        var destSR = destBuffer.getChannelData(3);
+
+        for (var i = 0; i < length; ++i) {
+            destL[i] += sourceL[i] + 0.7071 * sourceC[i];
+            destR[i] += sourceR[i] + 0.7071 * sourceC[i];
+            destSL[i] += sourceSL[i];
+            destSR[i] += sourceSR[i];
+        }
+    } else {
+        // Fallback for unknown combinations.
+        discreteSum(sourceBuffer, destBuffer);
+    }
+}
+
+function scheduleTest(testNumber, connections, channelCount, channelCountMode, channelInterpretation) {
+    var mixNode = context.createGain();
+    mixNode.channelCount = channelCount;
+    mixNode.channelCountMode = channelCountMode;
+    mixNode.channelInterpretation = channelInterpretation;
+    mixNode.connect(sp);
+
+    for (var i = 0; i < connections.length; ++i) {
+        var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0);
+
+        var source = context.createBufferSource();
+        // Get a buffer with the right number of channels, converting from 1-based to 0-based index.
+        var buffer = testBuffers[connectionNumberOfChannels - 1];
+        source.buffer = buffer;
+        source.connect(mixNode);
+
+        // Start at the right offset.
+        var sampleFrameOffset = testNumber * singleTestFrameLength;
+        var time = sampleFrameOffset / context.sampleRate;
+        source.start(time);
+    }
+}
+
+function computeNumberOfChannels(connections, channelCount, channelCountMode) {
+    if (channelCountMode == "explicit")
+        return channelCount;
+
+    var computedNumberOfChannels = 1; // Must have at least one channel.
+
+    // Compute "computedNumberOfChannels" based on all the connections.
+    for (var i = 0; i < connections.length; ++i) {
+        var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0);
+        computedNumberOfChannels = Math.max(computedNumberOfChannels, connectionNumberOfChannels);
+    }
+
+    if (channelCountMode == "clamped-max")
+        computedNumberOfChannels = Math.min(computedNumberOfChannels, channelCount);
+
+    return computedNumberOfChannels;
+}
+
+function checkTestResult(renderedBuffer, testNumber, connections, channelCount, channelCountMode, channelInterpretation) {
+    var computedNumberOfChannels = computeNumberOfChannels(connections, channelCount, channelCountMode);
+
+    // Create a zero-initialized silent AudioBuffer with computedNumberOfChannels.
+    var destBuffer = context.createBuffer(computedNumberOfChannels, singleTestFrameLength, context.sampleRate);
+
+    // Mix all of the connections into the destination buffer.
+    for (var i = 0; i < connections.length; ++i) {
+        var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0);
+        var 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 {
+            ok(false, "Invalid channel interpretation!");
+        }
+    }
+
+    // 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.
+
+    var sampleFrameOffset = testNumber * singleTestFrameLength;
+    for (var c = 0; c < renderNumberOfChannels; ++c) {
+        var renderedData = renderedBuffer.getChannelData(c);
+        for (var frame = 0; frame < singleTestFrameLength; ++frame) {
+            var renderedValue = renderedData[frame + sampleFrameOffset];
+
+            var expectedValue = 0;
+            if (c < destBuffer.numberOfChannels) {
+                var expectedData = destBuffer.getChannelData(c);
+                expectedValue = expectedData[frame];
+            }
+
+            if (Math.abs(renderedValue - expectedValue) > 1e-4) {
+                var s = "connections: " + connections + ", " + channelCountMode;
+
+                // channelCount is ignored in "max" mode.
+                if (channelCountMode == "clamped-max" || channelCountMode == "explicit") {
+                    s += "(" + channelCount + ")";
+                }
+
+                s += ", " + channelInterpretation + ". ";
+
+                var message = s + "rendered: " + renderedValue + " expected: " + expectedValue + " channel: " + c + " frame: " + frame;
+                is(renderedValue, expectedValue, message);
+            }
+        }
+    }
+}
+
+function checkResult(event) {
+    var buffer = event.inputBuffer;
+
+    // Sanity check result.
+    ok(buffer.length != numberOfTests * singleTestFrameLength ||
+       buffer.numberOfChannels != renderNumberOfChannels, "Sanity check");
+
+    // Check all the tests.
+    var testNumber = 0;
+    for (var m = 0; m < mixingRulesList.length; ++m) {
+        var mixingRules = mixingRulesList[m];
+        for (var i = 0; i < connectionsList.length; ++i, ++testNumber) {
+            checkTestResult(buffer, testNumber, connectionsList[i], mixingRules.channelCount, mixingRules.channelCountMode, mixingRules.channelInterpretation);
+        }
+    }
+
+    sp.onaudioprocess = null;
+    SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+function runTest() {
+    SpecialPowers.setBoolPref("media.webaudio.enabled", true);
+
+    // Create 8-channel offline audio context.
+    // Each test will render 8 sample-frames starting at sample-frame position testNumber * 8.
+    var totalFrameLength = numberOfTests * singleTestFrameLength;
+    context = new AudioContext();
+    var nextPowerOfTwo = 256;
+    while (nextPowerOfTwo < totalFrameLength) {
+        nextPowerOfTwo *= 2;
+    }
+    sp = context.createScriptProcessor(nextPowerOfTwo, renderNumberOfChannels);
+
+    // Set destination to discrete mixing.
+    sp.channelCount = renderNumberOfChannels;
+    sp.channelCountMode = "explicit";
+    sp.channelInterpretation = "discrete";
+
+    // Create test buffers from 1 to 8 channels.
+    testBuffers = new Array();
+    for (var i = 0; i < renderNumberOfChannels; ++i) {
+        testBuffers[i] = createTestBuffer(i + 1);
+    }
+
+    // Schedule all the tests.
+    var testNumber = 0;
+    for (var m = 0; m < mixingRulesList.length; ++m) {
+        var mixingRules = mixingRulesList[m];
+        for (var i = 0; i < connectionsList.length; ++i, ++testNumber) {
+            scheduleTest(testNumber, connectionsList[i], mixingRules.channelCount, mixingRules.channelCountMode, mixingRules.channelInterpretation);
+        }
+    }
+
+    // Render then check results.
+    sp.onaudioprocess = checkResult;
+}
+
+runTest();
+
+</script>
+
+</body>
+</html>