forked from mirrors/gecko-dev
Backed out changeset 9b8435c1c982 (bug 1860492) Backed out changeset 08603e5ea8a0 (bug 1860492) Backed out changeset 93086bc64d37 (bug 1860492) Backed out changeset f8cbb9933469 (bug 1860492) Backed out changeset f5e2a92235f1 (bug 1860492) Backed out changeset 0038d6d54690 (bug 1860492) Backed out changeset 24a1fb93d4a8 (bug 1860492) Backed out changeset c2c11ee3f79f (bug 1860492) Backed out changeset 9983c1ddee85 (bug 1860492) Backed out changeset b9286e049dea (bug 1860492) Backed out changeset d1d98783c88d (bug 1860492) Backed out changeset 22dd17861e80 (bug 1860492) Backed out changeset 7d823668fba7 (bug 1860492) Backed out changeset 024863677345 (bug 1860492)
436 lines
14 KiB
HTML
436 lines
14 KiB
HTML
<!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 },
|
|
// 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,
|
|
},
|
|
{
|
|
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
|
|
}
|
|
];
|
|
|
|
// 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;
|
|
}
|
|
|
|
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) {
|
|
ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data for " + test.url);
|
|
}
|
|
}
|
|
|
|
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, 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>
|