dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html
author Ting-Yu Lin <tlin@mozilla.com>
Sat, 01 Apr 2023 04:17:05 +0000
changeset 658805 7437637d0b5c2745440bfeba4adb64689d24044f
parent 431893 5a18b9c9a5a610cded246291c1c80498eba9dcfb
permissions -rw-r--r--
Bug 1055894 - Add GetLogicalNormalRect() and adapt some callers of GetNormalRect(). r=emilio This patch makes the API nicer, and shouldn't change the behavior. Differential Revision: https://phabricator.services.mozilla.com/D174344

<!DOCTYPE html>
<title>Test up-mixing in ConvolverNode after ChannelInterpretation change</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
// This test is not in wpt because it requires that multiple changes to the
// nodes in an AudioContext during a single event will be processed by the
// audio thread in a single transaction.  Gecko provides that, but this is not
// currently required by the Web Audio API.

const EPSILON = Math.pow(2, -23);
// sampleRate is a power of two so that delay times are exact in base-2
// floating point arithmetic.
const SAMPLE_RATE = 32768;
// Length of initial mono signal in frames, if the test has an initial mono
// signal.  This is more than one block to ensure that at least one block
// will be mono, even if interpolation in the DelayNode means that stereo is
// output one block earlier than if frames are delayed without interpolation.
const MONO_FRAMES = 256;
// Length of response buffer.  This is greater than 1 to ensure that the
// convolver has stereo output at least one block after stereo input is
// disconnected.
const RESPONSE_FRAMES = 2;

function test_interpretation_change(t, initialInterpretation, initialMonoFrames)
{
  let context = new AudioContext({sampleRate: SAMPLE_RATE});

  // Three independent signals.  These are constant so that results are
  // independent of the timing of the `ended` event.
  let monoOffset = 0.25
  let monoSource = new ConstantSourceNode(context, {offset: monoOffset});
  let leftOffset = 0.125;
  let rightOffset = 0.5;
  let leftSource = new ConstantSourceNode(context, {offset: leftOffset});
  let rightSource = new ConstantSourceNode(context, {offset: rightOffset});
  monoSource.start();
  leftSource.start();
  rightSource.start();

  let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
  leftSource.connect(stereoMerger, 0, 0);
  rightSource.connect(stereoMerger, 0, 1);

  // The DelayNode initially has a single channel of silence, and so the
  // output of the delay node is first mono silence (if there is a non-zero
  // initialMonoFrames), then stereo.  In Gecko, this triggers a convolver
  // configuration that is different for different channelInterpretations.
  let delay =
      new DelayNode(context,
                    {maxDelayTime: MONO_FRAMES / context.sampleRate,
                     delayTime: initialMonoFrames / context.sampleRate});
  stereoMerger.connect(delay);

  // Two convolvers with the same impulse response.  The test convolver will
  // process a mix of stereo and mono signals.  The reference convolver will
  // always process stereo, including the up-mixed mono signal.
  let response = new AudioBuffer({numberOfChannels: 1,
                                  length: RESPONSE_FRAMES,
                                  sampleRate: context.sampleRate});
  response.getChannelData(0)[response.length - 1] = 1;

  let testConvolver = new ConvolverNode(context,
                                        {disableNormalization: true,
                                         buffer: response});
  testConvolver.channelInterpretation = initialInterpretation;
  let referenceConvolver = new ConvolverNode(context,
                                             {disableNormalization: true,
                                              buffer: response});
  // No need to set referenceConvolver.channelInterpretation because
  // input is always stereo, due to up-mixing at gain node.
  let referenceMixer = new GainNode(context);
  referenceMixer.channelCount = 2;
  referenceMixer.channelCountMode = "explicit";
  referenceMixer.channelInterpretation = initialInterpretation;
  referenceMixer.connect(referenceConvolver);

  delay.connect(testConvolver);
  delay.connect(referenceMixer);

  monoSource.connect(testConvolver);
  monoSource.connect(referenceMixer);

  // A timer sends 'ended' when the convolvers are known to be processing
  // stereo.
  let timer = new ConstantSourceNode(context);
  timer.start();
  timer.stop((initialMonoFrames + 1) / context.sampleRate);

  timer.onended = t.step_func(() => {
    let changedInterpretation =
        initialInterpretation == "speakers" ? "discrete" : "speakers";

    // Switch channelInterpretation in test and reference paths.
    testConvolver.channelInterpretation = changedInterpretation;
    referenceMixer.channelInterpretation = changedInterpretation;

    // Disconnect the stereo input from both test and reference convolvers.
    // The disconnected convolvers will continue to output stereo for at least
    // one frame.  The test convolver will up-mix its mono input into its two
    // buffers.
    delay.disconnect();

    // Capture the outputs in a script processor.
    //
    // The first two channels contain signal where some up-mixing occurs
    // internally to the test convolver.
    //
    // The last two channels are expected to contain the same signal, but
    // up-mixing was performed at a GainNode prior to convolution.
    //
    // Two stereo splitters will collect test and reference outputs.
    let testSplitter =
        new ChannelSplitterNode(context, {numberOfOutputs: 2});
    let referenceSplitter =
        new ChannelSplitterNode(context, {numberOfOutputs: 2});
    testConvolver.connect(testSplitter);
    referenceConvolver.connect(referenceSplitter);

    let outputMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
    testSplitter.connect(outputMerger, 0, 0);
    testSplitter.connect(outputMerger, 1, 1);
    referenceSplitter.connect(outputMerger, 0, 2);
    referenceSplitter.connect(outputMerger, 1, 3);

    let processor = context.createScriptProcessor(256, 4, 0);
    outputMerger.connect(processor);

    processor.onaudioprocess = t.step_func_done((e) => {
      e.target.onaudioprocess = null;
      outputMerger.disconnect();

      // The test convolver output is stereo for the first block.
      let length = 128;

      let buffer = e.inputBuffer;
      let maxDiff = -1.0;
      let frameIndex = 0;
      let channelIndex = 0;
      for (let c = 0; c < 2; ++c) {
        let testOutput = buffer.getChannelData(0 + c);
        let referenceOutput = buffer.getChannelData(2 + c);
        for (var i = 0; i < length; ++i) {
          var diff = Math.abs(testOutput[i] - referenceOutput[i]);
          if (diff > maxDiff) {
            maxDiff = diff;
            frameIndex = i;
            channelIndex = c;
          }
        }
      }
      assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
                           buffer.getChannelData(2 + channelIndex)[frameIndex],
                           EPSILON,
                           `output at ${frameIndex} ` +
                             `in channel ${channelIndex}` );
    });
  });
}

async_test((t) => test_interpretation_change(t, "speakers", MONO_FRAMES),
           "speakers to discrete, initially mono");
async_test((t) => test_interpretation_change(t, "discrete", MONO_FRAMES),
           "discrete to speakers");
// Gecko uses a separate path for "speakers" initial up-mixing when the
// convolver's first input is stereo, so test that separately.
async_test((t) => test_interpretation_change(t, "speakers", 0),
           "speakers to discrete, initially stereo");
</script>