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