1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/content/media/webaudio/test/test_mediaDecoding.html Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,391 @@ 1.4 +<!DOCTYPE HTML> 1.5 +<html> 1.6 +<head> 1.7 + <title>Test the decodeAudioData API and Resampling</title> 1.8 + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> 1.9 + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 1.10 +</head> 1.11 +<body> 1.12 +<pre id="test"> 1.13 +<script src="webaudio.js" type="text/javascript"></script> 1.14 +<script type="text/javascript"> 1.15 + 1.16 +// These routines have been copied verbatim from WebKit, and are used in order 1.17 +// to convert a memory buffer into a wave buffer. 1.18 +function writeString(s, a, offset) { 1.19 + for (var i = 0; i < s.length; ++i) { 1.20 + a[offset + i] = s.charCodeAt(i); 1.21 + } 1.22 +} 1.23 + 1.24 +function writeInt16(n, a, offset) { 1.25 + n = Math.floor(n); 1.26 + 1.27 + var b1 = n & 255; 1.28 + var b2 = (n >> 8) & 255; 1.29 + 1.30 + a[offset + 0] = b1; 1.31 + a[offset + 1] = b2; 1.32 +} 1.33 + 1.34 +function writeInt32(n, a, offset) { 1.35 + n = Math.floor(n); 1.36 + var b1 = n & 255; 1.37 + var b2 = (n >> 8) & 255; 1.38 + var b3 = (n >> 16) & 255; 1.39 + var b4 = (n >> 24) & 255; 1.40 + 1.41 + a[offset + 0] = b1; 1.42 + a[offset + 1] = b2; 1.43 + a[offset + 2] = b3; 1.44 + a[offset + 3] = b4; 1.45 +} 1.46 + 1.47 +function writeAudioBuffer(audioBuffer, a, offset) { 1.48 + var n = audioBuffer.length; 1.49 + var channels = audioBuffer.numberOfChannels; 1.50 + 1.51 + for (var i = 0; i < n; ++i) { 1.52 + for (var k = 0; k < channels; ++k) { 1.53 + var buffer = audioBuffer.getChannelData(k); 1.54 + var sample = buffer[i] * 32768.0; 1.55 + 1.56 + // Clip samples to the limitations of 16-bit. 1.57 + // If we don't do this then we'll get nasty wrap-around distortion. 1.58 + if (sample < -32768) 1.59 + sample = -32768; 1.60 + if (sample > 32767) 1.61 + sample = 32767; 1.62 + 1.63 + writeInt16(sample, a, offset); 1.64 + offset += 2; 1.65 + } 1.66 + } 1.67 +} 1.68 + 1.69 +function createWaveFileData(audioBuffer) { 1.70 + var frameLength = audioBuffer.length; 1.71 + var numberOfChannels = audioBuffer.numberOfChannels; 1.72 + var sampleRate = audioBuffer.sampleRate; 1.73 + var bitsPerSample = 16; 1.74 + var byteRate = sampleRate * numberOfChannels * bitsPerSample/8; 1.75 + var blockAlign = numberOfChannels * bitsPerSample/8; 1.76 + var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio 1.77 + var headerByteLength = 44; 1.78 + var totalLength = headerByteLength + wavDataByteLength; 1.79 + 1.80 + var waveFileData = new Uint8Array(totalLength); 1.81 + 1.82 + var subChunk1Size = 16; // for linear PCM 1.83 + var subChunk2Size = wavDataByteLength; 1.84 + var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); 1.85 + 1.86 + writeString("RIFF", waveFileData, 0); 1.87 + writeInt32(chunkSize, waveFileData, 4); 1.88 + writeString("WAVE", waveFileData, 8); 1.89 + writeString("fmt ", waveFileData, 12); 1.90 + 1.91 + writeInt32(subChunk1Size, waveFileData, 16); // SubChunk1Size (4) 1.92 + writeInt16(1, waveFileData, 20); // AudioFormat (2) 1.93 + writeInt16(numberOfChannels, waveFileData, 22); // NumChannels (2) 1.94 + writeInt32(sampleRate, waveFileData, 24); // SampleRate (4) 1.95 + writeInt32(byteRate, waveFileData, 28); // ByteRate (4) 1.96 + writeInt16(blockAlign, waveFileData, 32); // BlockAlign (2) 1.97 + writeInt32(bitsPerSample, waveFileData, 34); // BitsPerSample (4) 1.98 + 1.99 + writeString("data", waveFileData, 36); 1.100 + writeInt32(subChunk2Size, waveFileData, 40); // SubChunk2Size (4) 1.101 + 1.102 + // Write actual audio data starting at offset 44. 1.103 + writeAudioBuffer(audioBuffer, waveFileData, 44); 1.104 + 1.105 + return waveFileData; 1.106 +} 1.107 + 1.108 +</script> 1.109 +<script class="testbody" type="text/javascript"> 1.110 + 1.111 +SimpleTest.waitForExplicitFinish(); 1.112 + 1.113 +// fuzzTolerance and fuzzToleranceMobile are used to determine fuzziness 1.114 +// thresholds. They're needed to make sure that we can deal with neglibible 1.115 +// differences in the binary buffer caused as a result of resampling the 1.116 +// audio. fuzzToleranceMobile is typically larger on mobile platforms since 1.117 +// we do fixed-point resampling as opposed to floating-point resampling on 1.118 +// those platforms. 1.119 +var files = [ 1.120 + // An ogg file, 44.1khz, mono 1.121 + { 1.122 + url: "ting-44.1k-1ch.ogg", 1.123 + valid: true, 1.124 + expectedUrl: "ting-44.1k-1ch.wav", 1.125 + numberOfChannels: 1, 1.126 + frames: 30592, 1.127 + sampleRate: 44100, 1.128 + duration: 0.693, 1.129 + fuzzTolerance: 5, 1.130 + fuzzToleranceMobile: 1284 1.131 + }, 1.132 + // An ogg file, 44.1khz, stereo 1.133 + { 1.134 + url: "ting-44.1k-2ch.ogg", 1.135 + valid: true, 1.136 + expectedUrl: "ting-44.1k-2ch.wav", 1.137 + numberOfChannels: 2, 1.138 + frames: 30592, 1.139 + sampleRate: 44100, 1.140 + duration: 0.693, 1.141 + fuzzTolerance: 6, 1.142 + fuzzToleranceMobile: 2544 1.143 + }, 1.144 + // An ogg file, 48khz, mono 1.145 + { 1.146 + url: "ting-48k-1ch.ogg", 1.147 + valid: true, 1.148 + expectedUrl: "ting-48k-1ch.wav", 1.149 + numberOfChannels: 1, 1.150 + frames: 33297, 1.151 + sampleRate: 48000, 1.152 + duration: 0.693, 1.153 + fuzzTolerance: 5, 1.154 + fuzzToleranceMobile: 1388 1.155 + }, 1.156 + // An ogg file, 48khz, stereo 1.157 + { 1.158 + url: "ting-48k-2ch.ogg", 1.159 + valid: true, 1.160 + expectedUrl: "ting-48k-2ch.wav", 1.161 + numberOfChannels: 2, 1.162 + frames: 33297, 1.163 + sampleRate: 48000, 1.164 + duration: 0.693, 1.165 + fuzzTolerance: 14, 1.166 + fuzzToleranceMobile: 2752 1.167 + }, 1.168 + // Make sure decoding a wave file results in the same buffer (for both the 1.169 + // resampling and non-resampling cases) 1.170 + { 1.171 + url: "ting-44.1k-1ch.wav", 1.172 + valid: true, 1.173 + expectedUrl: "ting-44.1k-1ch.wav", 1.174 + numberOfChannels: 1, 1.175 + frames: 30592, 1.176 + sampleRate: 44100, 1.177 + duration: 0.693, 1.178 + fuzzTolerance: 0, 1.179 + fuzzToleranceMobile: 0 1.180 + }, 1.181 + { 1.182 + url: "ting-48k-1ch.wav", 1.183 + valid: true, 1.184 + expectedUrl: "ting-48k-1ch.wav", 1.185 + numberOfChannels: 1, 1.186 + frames: 33297, 1.187 + sampleRate: 48000, 1.188 + duration: 0.693, 1.189 + fuzzTolerance: 0, 1.190 + fuzzToleranceMobile: 0 1.191 + }, 1.192 + // // A wave file 1.193 + // //{ url: "24bit-44khz.wav", valid: true, expectedUrl: "24bit-44khz-expected.wav" }, 1.194 + // A non-audio file 1.195 + { url: "invalid.txt", valid: false, sampleRate: 44100 }, 1.196 + // A webm file with no audio 1.197 + { url: "noaudio.webm", valid: false, sampleRate: 48000 }, 1.198 + // A video ogg file with audio 1.199 + { 1.200 + url: "audio.ogv", 1.201 + valid: true, 1.202 + expectedUrl: "audio-expected.wav", 1.203 + numberOfChannels: 2, 1.204 + sampleRate: 44100, 1.205 + frames: 47680, 1.206 + duration: 1.0807, 1.207 + fuzzTolerance: 106, 1.208 + fuzzToleranceMobile: 3482 1.209 + } 1.210 +]; 1.211 + 1.212 +// Returns true if the memory buffers are less different that |fuzz| bytes 1.213 +function fuzzyMemcmp(buf1, buf2, fuzz) { 1.214 + var result = true; 1.215 + var difference = 0; 1.216 + is(buf1.length, buf2.length, "same length"); 1.217 + for (var i = 0; i < buf1.length; ++i) { 1.218 + if (Math.abs(buf1[i] - buf2[i])) { 1.219 + ++difference; 1.220 + } 1.221 + } 1.222 + if (difference > fuzz) { 1.223 + ok(false, "Expected at most " + fuzz + " bytes difference, found " + difference + " bytes"); 1.224 + } 1.225 + return difference <= fuzz; 1.226 +} 1.227 + 1.228 +function getFuzzTolerance(test) { 1.229 + var kIsMobile = 1.230 + navigator.userAgent.indexOf("Mobile") != -1 || // b2g 1.231 + navigator.userAgent.indexOf("Android") != -1; // android 1.232 + return kIsMobile ? test.fuzzToleranceMobile : test.fuzzTolerance; 1.233 +} 1.234 + 1.235 +function bufferIsSilent(buffer) { 1.236 + for (var i = 0; i < buffer.length; ++i) { 1.237 + if (buffer.getChannelData(0)[i] != 0) { 1.238 + return false; 1.239 + } 1.240 + } 1.241 + return true; 1.242 +} 1.243 + 1.244 +function checkAudioBuffer(buffer, test) { 1.245 + if (buffer.numberOfChannels != test.numberOfChannels) { 1.246 + is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels"); 1.247 + return; 1.248 + } 1.249 + ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration"); 1.250 + if (Math.abs(buffer.duration - test.duration) >= 1e-3) { 1.251 + ok(false, "got: " + buffer.duration + ", expected: " + test.duration); 1.252 + } 1.253 + is(buffer.sampleRate, test.sampleRate, "Correct sample rate"); 1.254 + is(buffer.length, test.frames, "Correct length"); 1.255 + 1.256 + var wave = createWaveFileData(buffer); 1.257 + ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data"); 1.258 +} 1.259 + 1.260 +function checkResampledBuffer(buffer, test, callback) { 1.261 + if (buffer.numberOfChannels != test.numberOfChannels) { 1.262 + is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels"); 1.263 + return; 1.264 + } 1.265 + ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration"); 1.266 + if (Math.abs(buffer.duration - test.duration) >= 1e-3) { 1.267 + ok(false, "got: " + buffer.duration + ", expected: " + test.duration); 1.268 + } 1.269 + // Take into account the resampling when checking the size 1.270 + var expectedLength = test.frames * buffer.sampleRate / test.sampleRate; 1.271 + ok(Math.abs(buffer.length - expectedLength) < 1.0, "Correct length", "got " + buffer.length + ", expected about " + expectedLength); 1.272 + 1.273 + // Playback the buffer in the original context, to resample back to the 1.274 + // original rate and compare with the decoded buffer without resampling. 1.275 + cx = test.nativeContext; 1.276 + var expected = cx.createBufferSource(); 1.277 + expected.buffer = test.expectedBuffer; 1.278 + expected.start(); 1.279 + var inverse = cx.createGain(); 1.280 + inverse.gain.value = -1; 1.281 + expected.connect(inverse); 1.282 + inverse.connect(cx.destination); 1.283 + var resampled = cx.createBufferSource(); 1.284 + resampled.buffer = buffer; 1.285 + resampled.start(); 1.286 + // This stop should do nothing, but it tests for bug 937475 1.287 + resampled.stop(test.frames / cx.sampleRate); 1.288 + resampled.connect(cx.destination); 1.289 + cx.oncomplete = function(e) { 1.290 + ok(!bufferIsSilent(e.renderedBuffer), "Expect buffer not silent"); 1.291 + // Resampling will lose the highest frequency components, so we should 1.292 + // pass the difference through a low pass filter. However, either the 1.293 + // input files don't have significant high frequency components or the 1.294 + // tolerance in compareBuffers() is too high to detect them. 1.295 + compareBuffers(e.renderedBuffer, 1.296 + cx.createBuffer(test.numberOfChannels, 1.297 + test.frames, test.sampleRate)); 1.298 + callback(); 1.299 + } 1.300 + cx.startRendering(); 1.301 +} 1.302 + 1.303 +function runResampling(test, response, callback) { 1.304 + var sampleRate = test.sampleRate == 44100 ? 48000 : 44100; 1.305 + var cx = new OfflineAudioContext(1, 1, sampleRate); 1.306 + cx.decodeAudioData(response, function onSuccess(asyncResult) { 1.307 + is(asyncResult.sampleRate, sampleRate, "Correct sample rate"); 1.308 + 1.309 + checkResampledBuffer(asyncResult, test, callback); 1.310 + }, function onFailure() { 1.311 + ok(false, "Expected successful decode with resample"); 1.312 + callback(); 1.313 + }); 1.314 +} 1.315 + 1.316 +function runTest(test, response, callback) { 1.317 + // We need to copy the array here, because decodeAudioData is going to neuter 1.318 + // the array. 1.319 + var compressedAudio = response.slice(0); 1.320 + var expectCallback = false; 1.321 + var cx = new OfflineAudioContext(test.numberOfChannels || 1, 1.322 + test.frames || 1, test.sampleRate); 1.323 + cx.decodeAudioData(response, function onSuccess(asyncResult) { 1.324 + ok(expectCallback, "Success callback should fire asynchronously"); 1.325 + ok(test.valid, "Did expect success for test " + test.url); 1.326 + 1.327 + checkAudioBuffer(asyncResult, test); 1.328 + 1.329 + test.expectedBuffer = asyncResult; 1.330 + test.nativeContext = cx; 1.331 + runResampling(test, compressedAudio, callback); 1.332 + }, function onFailure() { 1.333 + ok(expectCallback, "Failure callback should fire asynchronously"); 1.334 + ok(!test.valid, "Did expect failure for test " + test.url); 1.335 + callback(); 1.336 + }); 1.337 + expectCallback = true; 1.338 +} 1.339 + 1.340 +function loadTest(test, callback) { 1.341 + var xhr = new XMLHttpRequest(); 1.342 + xhr.open("GET", test.url, true); 1.343 + xhr.responseType = "arraybuffer"; 1.344 + xhr.onload = function() { 1.345 + var getExpected = new XMLHttpRequest(); 1.346 + getExpected.open("GET", test.expectedUrl, true); 1.347 + getExpected.responseType = "arraybuffer"; 1.348 + getExpected.onload = function() { 1.349 + test.expectedWaveData = new Uint8Array(getExpected.response); 1.350 + runTest(test, xhr.response, callback); 1.351 + }; 1.352 + getExpected.send(); 1.353 + }; 1.354 + xhr.send(); 1.355 +} 1.356 + 1.357 +function loadNextTest() { 1.358 + if (files.length) { 1.359 + loadTest(files.shift(), loadNextTest); 1.360 + } else { 1.361 + SimpleTest.finish(); 1.362 + } 1.363 +} 1.364 + 1.365 +// Run some simple tests first 1.366 +function callbackShouldNeverRun() { 1.367 + ok(false, "callback should not fire"); 1.368 +} 1.369 +(function() { 1.370 + var cx = new AudioContext(); 1.371 + expectTypeError(function() { 1.372 + cx.decodeAudioData(null, callbackShouldNeverRun, callbackShouldNeverRun); 1.373 + }); 1.374 + expectTypeError(function() { 1.375 + cx.decodeAudioData(undefined, callbackShouldNeverRun, callbackShouldNeverRun); 1.376 + }); 1.377 + expectTypeError(function() { 1.378 + cx.decodeAudioData(123, callbackShouldNeverRun, callbackShouldNeverRun); 1.379 + }); 1.380 + expectTypeError(function() { 1.381 + cx.decodeAudioData("buffer", callbackShouldNeverRun, callbackShouldNeverRun); 1.382 + }); 1.383 + expectTypeError(function() { 1.384 + cx.decodeAudioData(new Uint8Array(100), callbackShouldNeverRun, callbackShouldNeverRun); 1.385 + }); 1.386 +})(); 1.387 + 1.388 +// Now, let's get real! 1.389 +loadNextTest(); 1.390 + 1.391 +</script> 1.392 +</pre> 1.393 +</body> 1.394 +</html>