<!DOCTYPE HTML>
<html>
<head>
  <title>Test the decodeAudioData API and Resampling</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<pre id="test">
<script src="webaudio.js" type="text/javascript"></script>
<script type="text/javascript">

// These routines have been copied verbatim from WebKit, and are used in order
// to convert a memory buffer into a wave buffer.
function writeString(s, a, offset) {
    for (var i = 0; i < s.length; ++i) {
        a[offset + i] = s.charCodeAt(i);
    }
}

function writeInt16(n, a, offset) {
    n = Math.floor(n);

    var b1 = n & 255;
    var b2 = (n >> 8) & 255;

    a[offset + 0] = b1;
    a[offset + 1] = b2;
}

function writeInt32(n, a, offset) {
    n = Math.floor(n);
    var b1 = n & 255;
    var b2 = (n >> 8) & 255;
    var b3 = (n >> 16) & 255;
    var b4 = (n >> 24) & 255;

    a[offset + 0] = b1;
    a[offset + 1] = b2;
    a[offset + 2] = b3;
    a[offset + 3] = b4;
}

function writeAudioBuffer(audioBuffer, a, offset) {
    var n = audioBuffer.length;
    var channels = audioBuffer.numberOfChannels;

    for (var i = 0; i < n; ++i) {
        for (var k = 0; k < channels; ++k) {
            var buffer = audioBuffer.getChannelData(k);
            var sample = buffer[i] * 32768.0;

            // Clip samples to the limitations of 16-bit.
            // If we don't do this then we'll get nasty wrap-around distortion.
            if (sample < -32768)
                sample = -32768;
            if (sample > 32767)
                sample = 32767;

            writeInt16(sample, a, offset);
            offset += 2;
        }
    }
}

function createWaveFileData(audioBuffer) {
    var frameLength = audioBuffer.length;
    var numberOfChannels = audioBuffer.numberOfChannels;
    var sampleRate = audioBuffer.sampleRate;
    var bitsPerSample = 16;
    var byteRate = sampleRate * numberOfChannels * bitsPerSample/8;
    var blockAlign = numberOfChannels * bitsPerSample/8;
    var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio
    var headerByteLength = 44;
    var totalLength = headerByteLength + wavDataByteLength;

    var waveFileData = new Uint8Array(totalLength);

    var subChunk1Size = 16; // for linear PCM
    var subChunk2Size = wavDataByteLength;
    var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);

    writeString("RIFF", waveFileData, 0);
    writeInt32(chunkSize, waveFileData, 4);
    writeString("WAVE", waveFileData, 8);
    writeString("fmt ", waveFileData, 12);

    writeInt32(subChunk1Size, waveFileData, 16);      // SubChunk1Size (4)
    writeInt16(1, waveFileData, 20);                  // AudioFormat (2)
    writeInt16(numberOfChannels, waveFileData, 22);   // NumChannels (2)
    writeInt32(sampleRate, waveFileData, 24);         // SampleRate (4)
    writeInt32(byteRate, waveFileData, 28);           // ByteRate (4)
    writeInt16(blockAlign, waveFileData, 32);         // BlockAlign (2)
    writeInt32(bitsPerSample, waveFileData, 34);      // BitsPerSample (4)

    writeString("data", waveFileData, 36);
    writeInt32(subChunk2Size, waveFileData, 40);      // SubChunk2Size (4)

    // Write actual audio data starting at offset 44.
    writeAudioBuffer(audioBuffer, waveFileData, 44);

    return waveFileData;
}

</script>
<script class="testbody" type="text/javascript">

SimpleTest.waitForExplicitFinish();

// fuzzTolerance and fuzzToleranceMobile are used to determine fuzziness
// thresholds.  They're needed to make sure that we can deal with neglibible
// differences in the binary buffer caused as a result of resampling the
// audio.  fuzzToleranceMobile is typically larger on mobile platforms since
// we do fixed-point resampling as opposed to floating-point resampling on
// those platforms.
var files = [
  // An ogg file, 44.1khz, mono
  {
    url: "ting-44.1k-1ch.ogg",
    valid: true,
    expectedUrl: "ting-44.1k-1ch.wav",
    numberOfChannels: 1,
    frames: 30592,
    sampleRate: 44100,
    duration: 0.693,
    fuzzTolerance: 5,
    fuzzToleranceMobile: 1284
  },
  // An ogg file, 44.1khz, stereo
  {
    url: "ting-44.1k-2ch.ogg",
    valid: true,
    expectedUrl: "ting-44.1k-2ch.wav",
    numberOfChannels: 2,
    frames: 30592,
    sampleRate: 44100,
    duration: 0.693,
    fuzzTolerance: 6,
    fuzzToleranceMobile: 2544
  },
  // An ogg file, 48khz, mono
  {
    url: "ting-48k-1ch.ogg",
    valid: true,
    expectedUrl: "ting-48k-1ch.wav",
    numberOfChannels: 1,
    frames: 33297,
    sampleRate: 48000,
    duration: 0.693,
    fuzzTolerance: 5,
    fuzzToleranceMobile: 1388
  },
  // An ogg file, 48khz, stereo
  {
    url: "ting-48k-2ch.ogg",
    valid: true,
    expectedUrl: "ting-48k-2ch.wav",
    numberOfChannels: 2,
    frames: 33297,
    sampleRate: 48000,
    duration: 0.693,
    fuzzTolerance: 14,
    fuzzToleranceMobile: 2752
  },
  // Make sure decoding a wave file results in the same buffer (for both the
  // resampling and non-resampling cases)
  {
    url: "ting-44.1k-1ch.wav",
    valid: true,
    expectedUrl: "ting-44.1k-1ch.wav",
    numberOfChannels: 1,
    frames: 30592,
    sampleRate: 44100,
    duration: 0.693,
    fuzzTolerance: 0,
    fuzzToleranceMobile: 0
  },
  {
    url: "ting-48k-1ch.wav",
    valid: true,
    expectedUrl: "ting-48k-1ch.wav",
    numberOfChannels: 1,
    frames: 33297,
    sampleRate: 48000,
    duration: 0.693,
    fuzzTolerance: 0,
    fuzzToleranceMobile: 0
  },
  //  // A wave file
  //  //{ url: "24bit-44khz.wav", valid: true, expectedUrl: "24bit-44khz-expected.wav" },
  // A non-audio file
  { url: "invalid.txt", valid: false, sampleRate: 44100 },
  // A webm file with no audio
  { url: "noaudio.webm", valid: false, sampleRate: 48000 },
  // A video ogg file with audio
  {
    url: "audio.ogv",
    valid: true,
    expectedUrl: "audio-expected.wav",
    numberOfChannels: 2,
    sampleRate: 44100,
    frames: 47680,
    duration: 1.0807,
    fuzzTolerance: 106,
    fuzzToleranceMobile: 3482
  },
  {
    url: "nil-packet.ogg",
    expectedUrl: null,
    valid: true,
    numberOfChannels: 2,
    sampleRate: 48000,
    frames: 18600,
    duration: 0.3874,
  }
];

// Returns true if the memory buffers are less different that |fuzz| bytes
function fuzzyMemcmp(buf1, buf2, fuzz) {
  var result = true;
  var difference = 0;
  is(buf1.length, buf2.length, "same length");
  for (var i = 0; i < buf1.length; ++i) {
    if (Math.abs(buf1[i] - buf2[i])) {
      ++difference;
    }
  }
  if (difference > fuzz) {
    ok(false, "Expected at most " + fuzz + " bytes difference, found " + difference + " bytes");
  }
  return difference <= fuzz;
}

function getFuzzTolerance(test) {
  var kIsMobile =
    navigator.userAgent.includes("Mobile") || // b2g
    navigator.userAgent.includes("Android");  // android
  return kIsMobile ? test.fuzzToleranceMobile : test.fuzzTolerance;
}

function bufferIsSilent(buffer) {
  for (var i = 0; i < buffer.length; ++i) {
    if (buffer.getChannelData(0)[i] != 0) {
      return false;
    }
  }
  return true;
}

function checkAudioBuffer(buffer, test) {
  if (buffer.numberOfChannels != test.numberOfChannels) {
    is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels");
    return;
  }
  ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration");
  if (Math.abs(buffer.duration - test.duration) >= 1e-3) {
    ok(false, "got: " + buffer.duration  + ", expected: " + test.duration);
  }
  is(buffer.sampleRate, test.sampleRate, "Correct sample rate");
  is(buffer.length, test.frames, "Correct length");

  var wave = createWaveFileData(buffer);
  if (test.expectedWaveData) {
    ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data");
  }
}

function checkResampledBuffer(buffer, test, callback) {
  if (buffer.numberOfChannels != test.numberOfChannels) {
    is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels");
    return;
  }
  ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration");
  if (Math.abs(buffer.duration - test.duration) >= 1e-3) {
    ok(false, "got: " + buffer.duration  + ", expected: " + test.duration);
  }
  // Take into account the resampling when checking the size
  var expectedLength = test.frames * buffer.sampleRate / test.sampleRate;
  SimpleTest.ok(
    Math.abs(buffer.length - expectedLength) < 1.0,
    "Correct length - got " + buffer.length +
    ", expected about " + expectedLength
  );

  // Playback the buffer in the original context, to resample back to the
  // original rate and compare with the decoded buffer without resampling.
  cx = test.nativeContext;
  var expected = cx.createBufferSource();
  expected.buffer = test.expectedBuffer;
  expected.start();
  var inverse = cx.createGain();
  inverse.gain.value = -1;
  expected.connect(inverse);
  inverse.connect(cx.destination);
  var resampled = cx.createBufferSource();
  resampled.buffer = buffer;
  resampled.start();
  // This stop should do nothing, but it tests for bug 937475
  resampled.stop(test.frames / cx.sampleRate);
  resampled.connect(cx.destination);
  cx.oncomplete = function(e) {
    ok(!bufferIsSilent(e.renderedBuffer), "Expect buffer not silent");
    // Resampling will lose the highest frequency components, so we should
    // pass the difference through a low pass filter.  However, either the
    // input files don't have significant high frequency components or the
    // tolerance in compareBuffers() is too high to detect them.
    compareBuffers(e.renderedBuffer,
                   cx.createBuffer(test.numberOfChannels,
                                   test.frames, test.sampleRate));
    callback();
  }
  cx.startRendering();
}

function runResampling(test, response, callback) {
  var sampleRate = test.sampleRate == 44100 ? 48000 : 44100;
  var cx = new OfflineAudioContext(1, 1, sampleRate);
  cx.decodeAudioData(response, function onSuccess(asyncResult) {
    is(asyncResult.sampleRate, sampleRate, "Correct sample rate");

    checkResampledBuffer(asyncResult, test, callback);
  }, function onFailure() {
    ok(false, "Expected successful decode with resample");
    callback();
  });
}

function runTest(test, response, callback) {
  // We need to copy the array here, because decodeAudioData will detach the
  // array's buffer.
  var compressedAudio = response.slice(0);
  var expectCallback = false;
  var cx = new OfflineAudioContext(test.numberOfChannels || 1,
                                   test.frames || 1, test.sampleRate);
  cx.decodeAudioData(response, function onSuccess(asyncResult) {
    ok(expectCallback, "Success callback should fire asynchronously");
    ok(test.valid, "Did expect success for test " + test.url);

    checkAudioBuffer(asyncResult, test);

    test.expectedBuffer = asyncResult;
    test.nativeContext = cx;
    runResampling(test, compressedAudio, callback);
  }, function onFailure(e) {
    ok(e instanceof DOMException, "We want to see an exception here");
    is(e.name, "EncodingError", "Exception name matches");
    ok(expectCallback, "Failure callback should fire asynchronously");
    ok(!test.valid, "Did expect failure for test " + test.url);
    callback();
  });
  expectCallback = true;
}

function loadTest(test, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", test.url, true);
  xhr.responseType = "arraybuffer";
  xhr.onload = function() {
    if (!test.expectedUrl) {
      runTest(test, xhr.response, callback);
      return;
    }
    var getExpected = new XMLHttpRequest();
    getExpected.open("GET", test.expectedUrl, true);
    getExpected.responseType = "arraybuffer";
    getExpected.onload = function() {
      test.expectedWaveData = new Uint8Array(getExpected.response);
      runTest(test, xhr.response, callback);
    };
    getExpected.send();
  };
  xhr.send();
}

function loadNextTest() {
  if (files.length) {
    loadTest(files.shift(), loadNextTest);
  } else {
    SimpleTest.finish();
  }
}

loadNextTest();

</script>
</pre>
</body>
</html>
