Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | <!DOCTYPE HTML> |
michael@0 | 2 | <html> |
michael@0 | 3 | <head> |
michael@0 | 4 | <title>Test the decodeAudioData API and Resampling</title> |
michael@0 | 5 | <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> |
michael@0 | 6 | <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> |
michael@0 | 7 | </head> |
michael@0 | 8 | <body> |
michael@0 | 9 | <pre id="test"> |
michael@0 | 10 | <script src="webaudio.js" type="text/javascript"></script> |
michael@0 | 11 | <script type="text/javascript"> |
michael@0 | 12 | |
michael@0 | 13 | // These routines have been copied verbatim from WebKit, and are used in order |
michael@0 | 14 | // to convert a memory buffer into a wave buffer. |
michael@0 | 15 | function writeString(s, a, offset) { |
michael@0 | 16 | for (var i = 0; i < s.length; ++i) { |
michael@0 | 17 | a[offset + i] = s.charCodeAt(i); |
michael@0 | 18 | } |
michael@0 | 19 | } |
michael@0 | 20 | |
michael@0 | 21 | function writeInt16(n, a, offset) { |
michael@0 | 22 | n = Math.floor(n); |
michael@0 | 23 | |
michael@0 | 24 | var b1 = n & 255; |
michael@0 | 25 | var b2 = (n >> 8) & 255; |
michael@0 | 26 | |
michael@0 | 27 | a[offset + 0] = b1; |
michael@0 | 28 | a[offset + 1] = b2; |
michael@0 | 29 | } |
michael@0 | 30 | |
michael@0 | 31 | function writeInt32(n, a, offset) { |
michael@0 | 32 | n = Math.floor(n); |
michael@0 | 33 | var b1 = n & 255; |
michael@0 | 34 | var b2 = (n >> 8) & 255; |
michael@0 | 35 | var b3 = (n >> 16) & 255; |
michael@0 | 36 | var b4 = (n >> 24) & 255; |
michael@0 | 37 | |
michael@0 | 38 | a[offset + 0] = b1; |
michael@0 | 39 | a[offset + 1] = b2; |
michael@0 | 40 | a[offset + 2] = b3; |
michael@0 | 41 | a[offset + 3] = b4; |
michael@0 | 42 | } |
michael@0 | 43 | |
michael@0 | 44 | function writeAudioBuffer(audioBuffer, a, offset) { |
michael@0 | 45 | var n = audioBuffer.length; |
michael@0 | 46 | var channels = audioBuffer.numberOfChannels; |
michael@0 | 47 | |
michael@0 | 48 | for (var i = 0; i < n; ++i) { |
michael@0 | 49 | for (var k = 0; k < channels; ++k) { |
michael@0 | 50 | var buffer = audioBuffer.getChannelData(k); |
michael@0 | 51 | var sample = buffer[i] * 32768.0; |
michael@0 | 52 | |
michael@0 | 53 | // Clip samples to the limitations of 16-bit. |
michael@0 | 54 | // If we don't do this then we'll get nasty wrap-around distortion. |
michael@0 | 55 | if (sample < -32768) |
michael@0 | 56 | sample = -32768; |
michael@0 | 57 | if (sample > 32767) |
michael@0 | 58 | sample = 32767; |
michael@0 | 59 | |
michael@0 | 60 | writeInt16(sample, a, offset); |
michael@0 | 61 | offset += 2; |
michael@0 | 62 | } |
michael@0 | 63 | } |
michael@0 | 64 | } |
michael@0 | 65 | |
michael@0 | 66 | function createWaveFileData(audioBuffer) { |
michael@0 | 67 | var frameLength = audioBuffer.length; |
michael@0 | 68 | var numberOfChannels = audioBuffer.numberOfChannels; |
michael@0 | 69 | var sampleRate = audioBuffer.sampleRate; |
michael@0 | 70 | var bitsPerSample = 16; |
michael@0 | 71 | var byteRate = sampleRate * numberOfChannels * bitsPerSample/8; |
michael@0 | 72 | var blockAlign = numberOfChannels * bitsPerSample/8; |
michael@0 | 73 | var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio |
michael@0 | 74 | var headerByteLength = 44; |
michael@0 | 75 | var totalLength = headerByteLength + wavDataByteLength; |
michael@0 | 76 | |
michael@0 | 77 | var waveFileData = new Uint8Array(totalLength); |
michael@0 | 78 | |
michael@0 | 79 | var subChunk1Size = 16; // for linear PCM |
michael@0 | 80 | var subChunk2Size = wavDataByteLength; |
michael@0 | 81 | var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); |
michael@0 | 82 | |
michael@0 | 83 | writeString("RIFF", waveFileData, 0); |
michael@0 | 84 | writeInt32(chunkSize, waveFileData, 4); |
michael@0 | 85 | writeString("WAVE", waveFileData, 8); |
michael@0 | 86 | writeString("fmt ", waveFileData, 12); |
michael@0 | 87 | |
michael@0 | 88 | writeInt32(subChunk1Size, waveFileData, 16); // SubChunk1Size (4) |
michael@0 | 89 | writeInt16(1, waveFileData, 20); // AudioFormat (2) |
michael@0 | 90 | writeInt16(numberOfChannels, waveFileData, 22); // NumChannels (2) |
michael@0 | 91 | writeInt32(sampleRate, waveFileData, 24); // SampleRate (4) |
michael@0 | 92 | writeInt32(byteRate, waveFileData, 28); // ByteRate (4) |
michael@0 | 93 | writeInt16(blockAlign, waveFileData, 32); // BlockAlign (2) |
michael@0 | 94 | writeInt32(bitsPerSample, waveFileData, 34); // BitsPerSample (4) |
michael@0 | 95 | |
michael@0 | 96 | writeString("data", waveFileData, 36); |
michael@0 | 97 | writeInt32(subChunk2Size, waveFileData, 40); // SubChunk2Size (4) |
michael@0 | 98 | |
michael@0 | 99 | // Write actual audio data starting at offset 44. |
michael@0 | 100 | writeAudioBuffer(audioBuffer, waveFileData, 44); |
michael@0 | 101 | |
michael@0 | 102 | return waveFileData; |
michael@0 | 103 | } |
michael@0 | 104 | |
michael@0 | 105 | </script> |
michael@0 | 106 | <script class="testbody" type="text/javascript"> |
michael@0 | 107 | |
michael@0 | 108 | SimpleTest.waitForExplicitFinish(); |
michael@0 | 109 | |
michael@0 | 110 | // fuzzTolerance and fuzzToleranceMobile are used to determine fuzziness |
michael@0 | 111 | // thresholds. They're needed to make sure that we can deal with neglibible |
michael@0 | 112 | // differences in the binary buffer caused as a result of resampling the |
michael@0 | 113 | // audio. fuzzToleranceMobile is typically larger on mobile platforms since |
michael@0 | 114 | // we do fixed-point resampling as opposed to floating-point resampling on |
michael@0 | 115 | // those platforms. |
michael@0 | 116 | var files = [ |
michael@0 | 117 | // An ogg file, 44.1khz, mono |
michael@0 | 118 | { |
michael@0 | 119 | url: "ting-44.1k-1ch.ogg", |
michael@0 | 120 | valid: true, |
michael@0 | 121 | expectedUrl: "ting-44.1k-1ch.wav", |
michael@0 | 122 | numberOfChannels: 1, |
michael@0 | 123 | frames: 30592, |
michael@0 | 124 | sampleRate: 44100, |
michael@0 | 125 | duration: 0.693, |
michael@0 | 126 | fuzzTolerance: 5, |
michael@0 | 127 | fuzzToleranceMobile: 1284 |
michael@0 | 128 | }, |
michael@0 | 129 | // An ogg file, 44.1khz, stereo |
michael@0 | 130 | { |
michael@0 | 131 | url: "ting-44.1k-2ch.ogg", |
michael@0 | 132 | valid: true, |
michael@0 | 133 | expectedUrl: "ting-44.1k-2ch.wav", |
michael@0 | 134 | numberOfChannels: 2, |
michael@0 | 135 | frames: 30592, |
michael@0 | 136 | sampleRate: 44100, |
michael@0 | 137 | duration: 0.693, |
michael@0 | 138 | fuzzTolerance: 6, |
michael@0 | 139 | fuzzToleranceMobile: 2544 |
michael@0 | 140 | }, |
michael@0 | 141 | // An ogg file, 48khz, mono |
michael@0 | 142 | { |
michael@0 | 143 | url: "ting-48k-1ch.ogg", |
michael@0 | 144 | valid: true, |
michael@0 | 145 | expectedUrl: "ting-48k-1ch.wav", |
michael@0 | 146 | numberOfChannels: 1, |
michael@0 | 147 | frames: 33297, |
michael@0 | 148 | sampleRate: 48000, |
michael@0 | 149 | duration: 0.693, |
michael@0 | 150 | fuzzTolerance: 5, |
michael@0 | 151 | fuzzToleranceMobile: 1388 |
michael@0 | 152 | }, |
michael@0 | 153 | // An ogg file, 48khz, stereo |
michael@0 | 154 | { |
michael@0 | 155 | url: "ting-48k-2ch.ogg", |
michael@0 | 156 | valid: true, |
michael@0 | 157 | expectedUrl: "ting-48k-2ch.wav", |
michael@0 | 158 | numberOfChannels: 2, |
michael@0 | 159 | frames: 33297, |
michael@0 | 160 | sampleRate: 48000, |
michael@0 | 161 | duration: 0.693, |
michael@0 | 162 | fuzzTolerance: 14, |
michael@0 | 163 | fuzzToleranceMobile: 2752 |
michael@0 | 164 | }, |
michael@0 | 165 | // Make sure decoding a wave file results in the same buffer (for both the |
michael@0 | 166 | // resampling and non-resampling cases) |
michael@0 | 167 | { |
michael@0 | 168 | url: "ting-44.1k-1ch.wav", |
michael@0 | 169 | valid: true, |
michael@0 | 170 | expectedUrl: "ting-44.1k-1ch.wav", |
michael@0 | 171 | numberOfChannels: 1, |
michael@0 | 172 | frames: 30592, |
michael@0 | 173 | sampleRate: 44100, |
michael@0 | 174 | duration: 0.693, |
michael@0 | 175 | fuzzTolerance: 0, |
michael@0 | 176 | fuzzToleranceMobile: 0 |
michael@0 | 177 | }, |
michael@0 | 178 | { |
michael@0 | 179 | url: "ting-48k-1ch.wav", |
michael@0 | 180 | valid: true, |
michael@0 | 181 | expectedUrl: "ting-48k-1ch.wav", |
michael@0 | 182 | numberOfChannels: 1, |
michael@0 | 183 | frames: 33297, |
michael@0 | 184 | sampleRate: 48000, |
michael@0 | 185 | duration: 0.693, |
michael@0 | 186 | fuzzTolerance: 0, |
michael@0 | 187 | fuzzToleranceMobile: 0 |
michael@0 | 188 | }, |
michael@0 | 189 | // // A wave file |
michael@0 | 190 | // //{ url: "24bit-44khz.wav", valid: true, expectedUrl: "24bit-44khz-expected.wav" }, |
michael@0 | 191 | // A non-audio file |
michael@0 | 192 | { url: "invalid.txt", valid: false, sampleRate: 44100 }, |
michael@0 | 193 | // A webm file with no audio |
michael@0 | 194 | { url: "noaudio.webm", valid: false, sampleRate: 48000 }, |
michael@0 | 195 | // A video ogg file with audio |
michael@0 | 196 | { |
michael@0 | 197 | url: "audio.ogv", |
michael@0 | 198 | valid: true, |
michael@0 | 199 | expectedUrl: "audio-expected.wav", |
michael@0 | 200 | numberOfChannels: 2, |
michael@0 | 201 | sampleRate: 44100, |
michael@0 | 202 | frames: 47680, |
michael@0 | 203 | duration: 1.0807, |
michael@0 | 204 | fuzzTolerance: 106, |
michael@0 | 205 | fuzzToleranceMobile: 3482 |
michael@0 | 206 | } |
michael@0 | 207 | ]; |
michael@0 | 208 | |
michael@0 | 209 | // Returns true if the memory buffers are less different that |fuzz| bytes |
michael@0 | 210 | function fuzzyMemcmp(buf1, buf2, fuzz) { |
michael@0 | 211 | var result = true; |
michael@0 | 212 | var difference = 0; |
michael@0 | 213 | is(buf1.length, buf2.length, "same length"); |
michael@0 | 214 | for (var i = 0; i < buf1.length; ++i) { |
michael@0 | 215 | if (Math.abs(buf1[i] - buf2[i])) { |
michael@0 | 216 | ++difference; |
michael@0 | 217 | } |
michael@0 | 218 | } |
michael@0 | 219 | if (difference > fuzz) { |
michael@0 | 220 | ok(false, "Expected at most " + fuzz + " bytes difference, found " + difference + " bytes"); |
michael@0 | 221 | } |
michael@0 | 222 | return difference <= fuzz; |
michael@0 | 223 | } |
michael@0 | 224 | |
michael@0 | 225 | function getFuzzTolerance(test) { |
michael@0 | 226 | var kIsMobile = |
michael@0 | 227 | navigator.userAgent.indexOf("Mobile") != -1 || // b2g |
michael@0 | 228 | navigator.userAgent.indexOf("Android") != -1; // android |
michael@0 | 229 | return kIsMobile ? test.fuzzToleranceMobile : test.fuzzTolerance; |
michael@0 | 230 | } |
michael@0 | 231 | |
michael@0 | 232 | function bufferIsSilent(buffer) { |
michael@0 | 233 | for (var i = 0; i < buffer.length; ++i) { |
michael@0 | 234 | if (buffer.getChannelData(0)[i] != 0) { |
michael@0 | 235 | return false; |
michael@0 | 236 | } |
michael@0 | 237 | } |
michael@0 | 238 | return true; |
michael@0 | 239 | } |
michael@0 | 240 | |
michael@0 | 241 | function checkAudioBuffer(buffer, test) { |
michael@0 | 242 | if (buffer.numberOfChannels != test.numberOfChannels) { |
michael@0 | 243 | is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels"); |
michael@0 | 244 | return; |
michael@0 | 245 | } |
michael@0 | 246 | ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration"); |
michael@0 | 247 | if (Math.abs(buffer.duration - test.duration) >= 1e-3) { |
michael@0 | 248 | ok(false, "got: " + buffer.duration + ", expected: " + test.duration); |
michael@0 | 249 | } |
michael@0 | 250 | is(buffer.sampleRate, test.sampleRate, "Correct sample rate"); |
michael@0 | 251 | is(buffer.length, test.frames, "Correct length"); |
michael@0 | 252 | |
michael@0 | 253 | var wave = createWaveFileData(buffer); |
michael@0 | 254 | ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data"); |
michael@0 | 255 | } |
michael@0 | 256 | |
michael@0 | 257 | function checkResampledBuffer(buffer, test, callback) { |
michael@0 | 258 | if (buffer.numberOfChannels != test.numberOfChannels) { |
michael@0 | 259 | is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels"); |
michael@0 | 260 | return; |
michael@0 | 261 | } |
michael@0 | 262 | ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration"); |
michael@0 | 263 | if (Math.abs(buffer.duration - test.duration) >= 1e-3) { |
michael@0 | 264 | ok(false, "got: " + buffer.duration + ", expected: " + test.duration); |
michael@0 | 265 | } |
michael@0 | 266 | // Take into account the resampling when checking the size |
michael@0 | 267 | var expectedLength = test.frames * buffer.sampleRate / test.sampleRate; |
michael@0 | 268 | ok(Math.abs(buffer.length - expectedLength) < 1.0, "Correct length", "got " + buffer.length + ", expected about " + expectedLength); |
michael@0 | 269 | |
michael@0 | 270 | // Playback the buffer in the original context, to resample back to the |
michael@0 | 271 | // original rate and compare with the decoded buffer without resampling. |
michael@0 | 272 | cx = test.nativeContext; |
michael@0 | 273 | var expected = cx.createBufferSource(); |
michael@0 | 274 | expected.buffer = test.expectedBuffer; |
michael@0 | 275 | expected.start(); |
michael@0 | 276 | var inverse = cx.createGain(); |
michael@0 | 277 | inverse.gain.value = -1; |
michael@0 | 278 | expected.connect(inverse); |
michael@0 | 279 | inverse.connect(cx.destination); |
michael@0 | 280 | var resampled = cx.createBufferSource(); |
michael@0 | 281 | resampled.buffer = buffer; |
michael@0 | 282 | resampled.start(); |
michael@0 | 283 | // This stop should do nothing, but it tests for bug 937475 |
michael@0 | 284 | resampled.stop(test.frames / cx.sampleRate); |
michael@0 | 285 | resampled.connect(cx.destination); |
michael@0 | 286 | cx.oncomplete = function(e) { |
michael@0 | 287 | ok(!bufferIsSilent(e.renderedBuffer), "Expect buffer not silent"); |
michael@0 | 288 | // Resampling will lose the highest frequency components, so we should |
michael@0 | 289 | // pass the difference through a low pass filter. However, either the |
michael@0 | 290 | // input files don't have significant high frequency components or the |
michael@0 | 291 | // tolerance in compareBuffers() is too high to detect them. |
michael@0 | 292 | compareBuffers(e.renderedBuffer, |
michael@0 | 293 | cx.createBuffer(test.numberOfChannels, |
michael@0 | 294 | test.frames, test.sampleRate)); |
michael@0 | 295 | callback(); |
michael@0 | 296 | } |
michael@0 | 297 | cx.startRendering(); |
michael@0 | 298 | } |
michael@0 | 299 | |
michael@0 | 300 | function runResampling(test, response, callback) { |
michael@0 | 301 | var sampleRate = test.sampleRate == 44100 ? 48000 : 44100; |
michael@0 | 302 | var cx = new OfflineAudioContext(1, 1, sampleRate); |
michael@0 | 303 | cx.decodeAudioData(response, function onSuccess(asyncResult) { |
michael@0 | 304 | is(asyncResult.sampleRate, sampleRate, "Correct sample rate"); |
michael@0 | 305 | |
michael@0 | 306 | checkResampledBuffer(asyncResult, test, callback); |
michael@0 | 307 | }, function onFailure() { |
michael@0 | 308 | ok(false, "Expected successful decode with resample"); |
michael@0 | 309 | callback(); |
michael@0 | 310 | }); |
michael@0 | 311 | } |
michael@0 | 312 | |
michael@0 | 313 | function runTest(test, response, callback) { |
michael@0 | 314 | // We need to copy the array here, because decodeAudioData is going to neuter |
michael@0 | 315 | // the array. |
michael@0 | 316 | var compressedAudio = response.slice(0); |
michael@0 | 317 | var expectCallback = false; |
michael@0 | 318 | var cx = new OfflineAudioContext(test.numberOfChannels || 1, |
michael@0 | 319 | test.frames || 1, test.sampleRate); |
michael@0 | 320 | cx.decodeAudioData(response, function onSuccess(asyncResult) { |
michael@0 | 321 | ok(expectCallback, "Success callback should fire asynchronously"); |
michael@0 | 322 | ok(test.valid, "Did expect success for test " + test.url); |
michael@0 | 323 | |
michael@0 | 324 | checkAudioBuffer(asyncResult, test); |
michael@0 | 325 | |
michael@0 | 326 | test.expectedBuffer = asyncResult; |
michael@0 | 327 | test.nativeContext = cx; |
michael@0 | 328 | runResampling(test, compressedAudio, callback); |
michael@0 | 329 | }, function onFailure() { |
michael@0 | 330 | ok(expectCallback, "Failure callback should fire asynchronously"); |
michael@0 | 331 | ok(!test.valid, "Did expect failure for test " + test.url); |
michael@0 | 332 | callback(); |
michael@0 | 333 | }); |
michael@0 | 334 | expectCallback = true; |
michael@0 | 335 | } |
michael@0 | 336 | |
michael@0 | 337 | function loadTest(test, callback) { |
michael@0 | 338 | var xhr = new XMLHttpRequest(); |
michael@0 | 339 | xhr.open("GET", test.url, true); |
michael@0 | 340 | xhr.responseType = "arraybuffer"; |
michael@0 | 341 | xhr.onload = function() { |
michael@0 | 342 | var getExpected = new XMLHttpRequest(); |
michael@0 | 343 | getExpected.open("GET", test.expectedUrl, true); |
michael@0 | 344 | getExpected.responseType = "arraybuffer"; |
michael@0 | 345 | getExpected.onload = function() { |
michael@0 | 346 | test.expectedWaveData = new Uint8Array(getExpected.response); |
michael@0 | 347 | runTest(test, xhr.response, callback); |
michael@0 | 348 | }; |
michael@0 | 349 | getExpected.send(); |
michael@0 | 350 | }; |
michael@0 | 351 | xhr.send(); |
michael@0 | 352 | } |
michael@0 | 353 | |
michael@0 | 354 | function loadNextTest() { |
michael@0 | 355 | if (files.length) { |
michael@0 | 356 | loadTest(files.shift(), loadNextTest); |
michael@0 | 357 | } else { |
michael@0 | 358 | SimpleTest.finish(); |
michael@0 | 359 | } |
michael@0 | 360 | } |
michael@0 | 361 | |
michael@0 | 362 | // Run some simple tests first |
michael@0 | 363 | function callbackShouldNeverRun() { |
michael@0 | 364 | ok(false, "callback should not fire"); |
michael@0 | 365 | } |
michael@0 | 366 | (function() { |
michael@0 | 367 | var cx = new AudioContext(); |
michael@0 | 368 | expectTypeError(function() { |
michael@0 | 369 | cx.decodeAudioData(null, callbackShouldNeverRun, callbackShouldNeverRun); |
michael@0 | 370 | }); |
michael@0 | 371 | expectTypeError(function() { |
michael@0 | 372 | cx.decodeAudioData(undefined, callbackShouldNeverRun, callbackShouldNeverRun); |
michael@0 | 373 | }); |
michael@0 | 374 | expectTypeError(function() { |
michael@0 | 375 | cx.decodeAudioData(123, callbackShouldNeverRun, callbackShouldNeverRun); |
michael@0 | 376 | }); |
michael@0 | 377 | expectTypeError(function() { |
michael@0 | 378 | cx.decodeAudioData("buffer", callbackShouldNeverRun, callbackShouldNeverRun); |
michael@0 | 379 | }); |
michael@0 | 380 | expectTypeError(function() { |
michael@0 | 381 | cx.decodeAudioData(new Uint8Array(100), callbackShouldNeverRun, callbackShouldNeverRun); |
michael@0 | 382 | }); |
michael@0 | 383 | })(); |
michael@0 | 384 | |
michael@0 | 385 | // Now, let's get real! |
michael@0 | 386 | loadNextTest(); |
michael@0 | 387 | |
michael@0 | 388 | </script> |
michael@0 | 389 | </pre> |
michael@0 | 390 | </body> |
michael@0 | 391 | </html> |