Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!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.
// If fuzzMagnitude, is present, is the maximum magnitude difference, per
// sample, to consider two samples are identical. It is multiplied by the
// maximum value a sample, in our case INT16_MAX. This allows checking files
// that should be identical except one has e.g. a higher quantization noise.
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 },
{
url: "nil-packet.ogg",
expectedUrl: null,
valid: true,
numberOfChannels: 2,
sampleRate: 48000,
frames: 18600,
duration: 0.3874,
},
{
url: "half-a-second-1ch-44100-mulaw.wav",
// It is expected that mulaw and linear are similar enough at 16-bits
expectedUrl: "half-a-second-1ch-44100.wav",
valid: true,
numberOfChannels: 1,
sampleRate: 44100,
frames: 22050,
duration: 0.5,
fuzzMagnitude: 0.04,
},
{
url: "half-a-second-1ch-44100-alaw.wav",
// It is expected that alaw and linear are similar enough at 16-bits
expectedUrl: "half-a-second-1ch-44100.wav",
valid: true,
numberOfChannels: 1,
sampleRate: 44100,
frames: 22050,
duration: 0.5,
fuzzMagnitude: 0.04,
},
{
url: "waveformatextensible.wav",
valid: true,
numberOfChannels: 1,
sampleRate: 44100,
frames: 472,
duration: 0.01
},
{
// A wav file that has 8 channel, but has a channel mask that doesn't
// match the channel count.
url: "waveformatextensiblebadmask.wav",
valid: true,
numberOfChannels: 8,
sampleRate: 8000,
frames: 80,
duration: 0.01
},
{
// A wav file that has 8 channels, in f32le
url: "8ch-f32le.wav",
expectedUrl: "8ch-s16.wav", // similar enough
valid: true,
numberOfChannels: 8,
frames: 4800,
sampleRate: 48000,
duration: 0.1,
// Only compare decoded audio: the headers are different between the two files
compareDecoded: true
}
];
// Returns true if the memory buffers are less different that |fuzz| bytes
function fuzzyMemcmp(buf1, buf2, fuzz) {
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]) > fuzz.magnitude * (2 << 15)) {
++difference;
}
}
if (difference > fuzz.count) {
ok(false, "Expected at most " + fuzz + " bytes difference, found " + difference + " bytes");
}
console.log(difference, fuzz.count);
return difference <= fuzz.count;
}
function getFuzzTolerance(test) {
var kIsMobile =
navigator.userAgent.includes("Mobile") || // b2g
navigator.userAgent.includes("Android"); // android
return {
magnitude: test.fuzzMagnitude ?? 0,
count: kIsMobile ? test.fuzzToleranceMobile ?? 0 : test.fuzzTolerance ?? 0
};
}
function bufferIsSilent(buffer) {
for (var i = 0; i < buffer.length; ++i) {
if (buffer.getChannelData(0)[i] != 0) {
return false;
}
}
return true;
}
async 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 expected ${test.duration} got ${buffer.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 && !test.compareDecoded) {
ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data for " + test.url);
} else if (test.compareDecoded) {
var off = new OfflineAudioContext(1, 1, buffer.sampleRate);
var decodedReference = await off.decodeAudioData(test.expectedWaveData.buffer);
compareBuffers(buffer, decodedReference);
}
}
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.
let 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, async function onSuccess(asyncResult) {
ok(expectCallback, "Success callback should fire asynchronously");
ok(test.valid, "Did expect success for test " + test.url);
await 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>