tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>