<!DOCTYPE HTML> <html> <head> <title>Test the decodeAudioData API and Resampling</title> <script type="text/javascript" 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 } ]; // 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.indexOf("Mobile") != -1 || // b2g navigator.userAgent.indexOf("Android") != -1; // 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); 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; 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() { 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() { 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>