<!DOCTYPE html>
<title>Test that up-mixing signals in ConvolverNode processing is linear</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
const EPSILON = 3.0 * Math.pow(2, -22);
// sampleRate is a power of two so that delay times are exact in base-2
// floating point arithmetic.
const SAMPLE_RATE = 32768;
// Length of stereo convolver input in frames (arbitrary):
const STEREO_FRAMES = 256;
// Length of mono signal in frames.  This is more than two blocks to ensure
// that at least one block will be mono, even if interpolation in the
// DelayNode means that stereo is output one block earlier and later than
// if frames are delayed without interpolation.
const MONO_FRAMES = 384;
// Length of response buffer:
const RESPONSE_FRAMES = 256;

function test_linear_upmixing(channelInterpretation, initial_mono_frames)
{
  let stereo_input_end = initial_mono_frames + STEREO_FRAMES;
  // Total length:
  let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES;
  // The first two channels contain signal where some up-mixing occurs
  // internally to a ConvolverNode when a stereo signal is added and removed.
  // The last two channels are expected to contain the same signal, but mono
  // and stereo signals are convolved independently before up-mixing the mono
  // output to mix with the stereo output.
  let context = new OfflineAudioContext({numberOfChannels: 4,
                                         length: length,
                                         sampleRate: SAMPLE_RATE});

  let response = new AudioBuffer({numberOfChannels: 1,
                                  length: RESPONSE_FRAMES,
                                  sampleRate: context.sampleRate});

  // Two stereo channel splitters will collect test and reference outputs.
  let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
  destinationMerger.connect(context.destination);
  let testSplitter =
      new ChannelSplitterNode(context, {numberOfOutputs: 2});
  let referenceSplitter =
      new ChannelSplitterNode(context, {numberOfOutputs: 2});
  testSplitter.connect(destinationMerger, 0, 0);
  testSplitter.connect(destinationMerger, 1, 1);
  referenceSplitter.connect(destinationMerger, 0, 2);
  referenceSplitter.connect(destinationMerger, 1, 3);

  // A GainNode mixes reference stereo and mono signals because up-mixing
  // cannot be performed at a channel splitter.
  let referenceGain = new GainNode(context);
  referenceGain.connect(referenceSplitter);
  referenceGain.channelInterpretation = channelInterpretation;

  // The impulse response for convolution contains two impulses so as to test
  // effects in at least two processing blocks.
  response.getChannelData(0)[0] = 0.5;
  response.getChannelData(0)[response.length - 1] = 0.5;

  let testConvolver = new ConvolverNode(context, {disableNormalization: true,
                                                  buffer: response});
  testConvolver.channelInterpretation = channelInterpretation;
  let referenceMonoConvolver = new ConvolverNode(context,
                                                 {disableNormalization: true,
                                                  buffer: response});
  let referenceStereoConvolver = new ConvolverNode(context,
                                                   {disableNormalization: true,
                                                    buffer: response});
  // No need to set referenceStereoConvolver.channelInterpretation because
  // input is either silent or stereo.
  testConvolver.connect(testSplitter);
  // Mix reference convolver output.
  referenceMonoConvolver.connect(referenceGain);
  referenceStereoConvolver.connect(referenceGain);

  // The DelayNode initially has a single channel of silence, which is used to
  // switch the stereo signal in and out.  The output of the delay node is
  // first mono silence (if there is a non-zero initial_mono_frames), then
  // stereo, then mono silence, and finally stereo again.  maxDelayTime is
  // used to generate the middle mono silence period from the initial silence
  // in the DelayNode and then generate the final period of stereo from its
  // initial input.
  let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate;
  let delay =
      new DelayNode(context,
                    {maxDelayTime: maxDelayTime,
                     delayTime: initial_mono_frames / context.sampleRate});
  // Schedule an increase in the delay to return to mono silence.
  delay.delayTime.setValueAtTime(maxDelayTime,
                                 stereo_input_end / context.sampleRate);
  delay.connect(testConvolver);
  delay.connect(referenceStereoConvolver);

  let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
  stereoMerger.connect(delay);

  // Three independent signals
  let monoSignal = new OscillatorNode(context, {frequency: 440});
  let leftSignal = new OscillatorNode(context, {frequency: 450});
  let rightSignal = new OscillatorNode(context, {frequency: 460});
  monoSignal.connect(testConvolver);
  monoSignal.connect(referenceMonoConvolver);
  leftSignal.connect(stereoMerger, 0, 0);
  rightSignal.connect(stereoMerger, 0, 1);
  monoSignal.start();
  leftSignal.start();
  rightSignal.start();

  return context.startRendering().
    then((buffer) => {
      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 < buffer.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}` );
    });
}

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