<!doctype html>
<html>
  <head>
    <title>
      Test Sub-Sample Accurate Stitching of ABSNs
    </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>
  </head>
  <body>
    <script>
      let audit = Audit.createTaskRunner();

      audit.define(
          {
            label: 'buffer-stitching-1',
            description: 'Subsample buffer stitching, same rates'
          },
          (task, should) => {
            const sampleRate = 44100;
            const bufferRate = 44100;
            const bufferLength = 30;

            // Experimentally determined thresholds.  DO NOT relax these values
            // to far from these values to make the tests pass.
            const errorThreshold = 9.0957e-5;
            const snrThreshold = 85.580;

            // Informative message
            should(sampleRate, 'Test 1: context.sampleRate')
                .beEqualTo(sampleRate);
            testBufferStitching(sampleRate, bufferRate, bufferLength)
                .then(resultBuffer => {
                  const actual = resultBuffer.getChannelData(0);
                  const expected = resultBuffer.getChannelData(1);
                  should(
                      actual,
                      `Stitched sine-wave buffers at sample rate ${bufferRate}`)
                      .beCloseToArray(
                          expected, {absoluteThreshold: errorThreshold});
                  const SNR = 10 * Math.log10(computeSNR(actual, expected));
                  should(SNR, `SNR (${SNR} dB)`)
                      .beGreaterThanOrEqualTo(snrThreshold);
                })
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'buffer-stitching-2',
            description: 'Subsample buffer stitching, different rates'
          },
          (task, should) => {
            const sampleRate = 44100;
            const bufferRate = 43800;
            const bufferLength = 30;

            // Experimentally determined thresholds.  DO NOT relax these values
            // to far from these values to make the tests pass.
            const errorThreshold = 3.8986e-3;
            const snrThreshold = 65.737;

            // Informative message
            should(sampleRate, 'Test 2: context.sampleRate')
                .beEqualTo(sampleRate);
            testBufferStitching(sampleRate, bufferRate, bufferLength)
                .then(resultBuffer => {
                  const actual = resultBuffer.getChannelData(0);
                  const expected = resultBuffer.getChannelData(1);
                  should(
                      actual,
                      `Stitched sine-wave buffers at sample rate ${bufferRate}`)
                      .beCloseToArray(
                          expected, {absoluteThreshold: errorThreshold});
                  const SNR = 10 * Math.log10(computeSNR(actual, expected));
                  should(SNR, `SNR (${SNR} dB)`)
                      .beGreaterThanOrEqualTo(snrThreshold);
                })
                .then(() => task.done());
          });

      audit.run();

      // Create graph to test stitching of consecutive ABSNs.  The context rate
      // is |sampleRate|, and the buffers have a fixed length of |bufferLength|
      // and rate of |bufferRate|.  The |bufferRate| should not be too different
      // from |sampleRate| because of interpolation of the buffer to the context
      // rate.
      function testBufferStitching(sampleRate, bufferRate, bufferLength) {
        // The context for testing.  Channel 0 contains the output from
        // stitching all the buffers together, and channel 1 contains the
        // expected output.
        const context = new OfflineAudioContext(
            {numberOfChannels: 2, length: sampleRate, sampleRate: sampleRate});

        const merger = new ChannelMergerNode(
            context, {numberOfInputs: context.destination.channelCount});

        merger.connect(context.destination);

        // The reference is a sine wave at 440 Hz.
        const ref = new OscillatorNode(context, {frequency: 440, type: 'sine'});
        ref.connect(merger, 0, 1);
        ref.start();

        // The test signal is a bunch of short AudioBufferSources containing
        // bits of a sine wave.
        let waveSignal = new Float32Array(context.length);
        const omega = 2 * Math.PI / bufferRate * ref.frequency.value;
        for (let k = 0; k < context.length; ++k) {
          waveSignal[k] = Math.sin(omega * k);
        }

        // Slice the sine wave into many little buffers to be assigned to ABSNs
        // that are started at the appropriate times to produce a final sine
        // wave.
        for (let k = 0; k < context.length; k += bufferLength) {
          const buffer =
              new AudioBuffer({length: bufferLength, sampleRate: bufferRate});
          buffer.copyToChannel(waveSignal.slice(k, k + bufferLength), 0);

          const src = new AudioBufferSourceNode(context, {buffer: buffer});
          src.connect(merger, 0, 0);
          src.start(k / bufferRate);
        }

        return context.startRendering();
      }
    </script>
  </body>
</html>
