tor-browser

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

sub-sample-scheduling.html (17393B)


      1 <!doctype html>
      2 <html>
      3  <head>
      4    <title>
      5      Test Sub-Sample Accurate Scheduling for ABSN
      6    </title>
      7    <script src="/resources/testharness.js"></script>
      8    <script src="/resources/testharnessreport.js"></script>
      9    <script src="/webaudio/resources/audit-util.js"></script>
     10    <script src="/webaudio/resources/audit.js"></script>
     11  </head>
     12  <body>
     13    <script>
     14      // Power of two so there's no roundoff converting from integer frames to
     15      // time.
     16      let sampleRate = 32768;
     17 
     18      let audit = Audit.createTaskRunner();
     19 
     20      audit.define('sub-sample accurate start', (task, should) => {
     21        // There are two channels, one for each source.  Only need to render
     22        // quanta for this test.
     23        let context = new OfflineAudioContext(
     24            {numberOfChannels: 2, length: 8192, sampleRate: sampleRate});
     25        let merger = new ChannelMergerNode(
     26            context, {numberOfInputs: context.destination.channelCount});
     27 
     28        merger.connect(context.destination);
     29 
     30        // Use a simple linear ramp for the sources with integer steps starting
     31        // at 1 to make it easy to verify and test that have sub-sample accurate
     32        // start.  Ramp starts at 1 so we can easily tell when the source
     33        // starts.
     34        let rampBuffer = new AudioBuffer(
     35            {length: context.length, sampleRate: context.sampleRate});
     36        let r = rampBuffer.getChannelData(0);
     37        for (let k = 0; k < r.length; ++k) {
     38          r[k] = k + 1;
     39        }
     40 
     41        const src0 = new AudioBufferSourceNode(context, {buffer: rampBuffer});
     42        const src1 = new AudioBufferSourceNode(context, {buffer: rampBuffer});
     43 
     44        // Frame where sources should start. This is pretty arbitrary, but one
     45        // should be close to an integer and the other should be close to the
     46        // next integer.  We do this to catch the case where rounding of the
     47        // start frame is being done.  Rounding is incorrect.
     48        const startFrame = 33;
     49        const startFrame0 = startFrame + 0.1;
     50        const startFrame1 = startFrame + 0.9;
     51 
     52        src0.connect(merger, 0, 0);
     53        src1.connect(merger, 0, 1);
     54 
     55        src0.start(startFrame0 / context.sampleRate);
     56        src1.start(startFrame1 / context.sampleRate);
     57 
     58        context.startRendering()
     59            .then(audioBuffer => {
     60              const output0 = audioBuffer.getChannelData(0);
     61              const output1 = audioBuffer.getChannelData(1);
     62 
     63              // Compute the expected output by interpolating the ramp buffer of
     64              // the sources if they started at the given frame.
     65              const ramp = rampBuffer.getChannelData(0);
     66              const expected0 = interpolateRamp(ramp, startFrame0);
     67              const expected1 = interpolateRamp(ramp, startFrame1);
     68 
     69              // Verify output0 has the correct values
     70 
     71              // For information only
     72              should(startFrame0, 'src0 start frame').beEqualTo(startFrame0);
     73 
     74              // Output must be zero before the source start frame, and it must
     75              // be interpolated correctly after the start frame.  The
     76              // absoluteThreshold below is currently set for Chrome which does
     77              // linear interpolation.  This needs to be updated eventually if
     78              // other browsers do not user interpolation.
     79              should(
     80                  output0.slice(0, startFrame + 1), `output0[0:${startFrame}]`)
     81                  .beConstantValueOf(0);
     82              should(
     83                  output0.slice(startFrame + 1, expected0.length),
     84                  `output0[${startFrame + 1}:${expected0.length - 1}]`)
     85                  .beCloseToArray(
     86                      expected0.slice(startFrame + 1), {absoluteThreshold: 0});
     87 
     88              // Verify output1 has the correct values.  Same approach as for
     89              // output0.
     90              should(startFrame1, 'src1 start frame').beEqualTo(startFrame1);
     91 
     92              should(
     93                  output1.slice(0, startFrame + 1), `output1[0:${startFrame}]`)
     94                  .beConstantValueOf(0);
     95              should(
     96                  output1.slice(startFrame + 1, expected1.length),
     97                  `output1[${startFrame + 1}:${expected1.length - 1}]`)
     98                  .beCloseToArray(
     99                      expected1.slice(startFrame + 1), {absoluteThreshold: 0});
    100            })
    101            .then(() => task.done());
    102      });
    103 
    104      audit.define('sub-sample accurate stop', (task, should) => {
    105        // There are threes channesl, one for each source.  Only need to render
    106        // quanta for this test.
    107        let context = new OfflineAudioContext(
    108            {numberOfChannels: 3, length: 128, sampleRate: sampleRate});
    109        let merger = new ChannelMergerNode(
    110            context, {numberOfInputs: context.destination.channelCount});
    111 
    112        merger.connect(context.destination);
    113 
    114        // The source can be as simple constant for this test.
    115        let buffer = new AudioBuffer(
    116            {length: context.length, sampleRate: context.sampleRate});
    117        buffer.getChannelData(0).fill(1);
    118 
    119        const src0 = new AudioBufferSourceNode(context, {buffer: buffer});
    120        const src1 = new AudioBufferSourceNode(context, {buffer: buffer});
    121        const src2 = new AudioBufferSourceNode(context, {buffer: buffer});
    122 
    123        // Frame where sources should start. This is pretty arbitrary, but one
    124        // should be an integer, one should be close to an integer and the other
    125        // should be close to the next integer.  This is to catch the case where
    126        // rounding is used for the end frame.  Rounding is incorrect.
    127        const endFrame = 33;
    128        const endFrame1 = endFrame + 0.1;
    129        const endFrame2 = endFrame + 0.9;
    130 
    131        src0.connect(merger, 0, 0);
    132        src1.connect(merger, 0, 1);
    133        src2.connect(merger, 0, 2);
    134 
    135        src0.start(0);
    136        src1.start(0);
    137        src2.start(0);
    138        src0.stop(endFrame / context.sampleRate);
    139        src1.stop(endFrame1 / context.sampleRate);
    140        src2.stop(endFrame2 / context.sampleRate);
    141 
    142        context.startRendering()
    143          .then(audioBuffer => {
    144            let actual0 = audioBuffer.getChannelData(0);
    145            let actual1 = audioBuffer.getChannelData(1);
    146            let actual2 = audioBuffer.getChannelData(2);
    147 
    148            // Just verify that we stopped at the right time.
    149 
    150            // This is case where the end frame is an integer.  Since the first
    151            // output ends on an exact frame, the output must be zero at that
    152            // frame number.  We print the end frame for information only; it
    153            // makes interpretation of the rest easier.
    154            should(endFrame - 1, 'src0 end frame')
    155              .beEqualTo(endFrame - 1);
    156            should(actual0[endFrame - 1], `output0[${endFrame - 1}]`)
    157              .notBeEqualTo(0);
    158            should(actual0.slice(endFrame),
    159                   `output0[${endFrame}:]`)
    160              .beConstantValueOf(0);
    161 
    162            // The case where the end frame is just a little above an integer.
    163            // The output must not be zero just before the end and must be zero
    164            // after.
    165            should(endFrame1, 'src1 end frame')
    166              .beEqualTo(endFrame1);
    167            should(actual1[endFrame], `output1[${endFrame}]`)
    168              .notBeEqualTo(0);
    169            should(actual1.slice(endFrame + 1),
    170                   `output1[${endFrame + 1}:]`)
    171              .beConstantValueOf(0);
    172 
    173            // The case where the end frame is just a little below an integer.
    174            // The output must not be zero just before the end and must be zero
    175            // after.
    176            should(endFrame2, 'src2 end frame')
    177              .beEqualTo(endFrame2);
    178            should(actual2[endFrame], `output2[${endFrame}]`)
    179              .notBeEqualTo(0);
    180            should(actual2.slice(endFrame + 1),
    181                   `output2[${endFrame + 1}:]`)
    182              .beConstantValueOf(0);
    183          })
    184          .then(() => task.done());
    185      });
    186 
    187      audit.define('sub-sample-grain', (task, should) => {
    188        let context = new OfflineAudioContext(
    189            {numberOfChannels: 2, length: 128, sampleRate: sampleRate});
    190 
    191        let merger = new ChannelMergerNode(
    192            context, {numberOfInputs: context.destination.channelCount});
    193 
    194        merger.connect(context.destination);
    195 
    196        // The source can be as simple constant for this test.
    197        let buffer = new AudioBuffer(
    198            {length: context.length, sampleRate: context.sampleRate});
    199        buffer.getChannelData(0).fill(1);
    200 
    201        let src0 = new AudioBufferSourceNode(context, {buffer: buffer});
    202        let src1 = new AudioBufferSourceNode(context, {buffer: buffer});
    203 
    204        src0.connect(merger, 0, 0);
    205        src1.connect(merger, 0, 1);
    206 
    207        // Start a short grain.
    208        const src0StartGrain = 3.1;
    209        const src0EndGrain = 37.2;
    210        src0.start(
    211            src0StartGrain / context.sampleRate, 0,
    212            (src0EndGrain - src0StartGrain) / context.sampleRate);
    213 
    214        const src1StartGrain = 5.8;
    215        const src1EndGrain = 43.9;
    216        src1.start(
    217            src1StartGrain / context.sampleRate, 0,
    218            (src1EndGrain - src1StartGrain) / context.sampleRate);
    219 
    220        context.startRendering()
    221            .then(audioBuffer => {
    222              let output0 = audioBuffer.getChannelData(0);
    223              let output1 = audioBuffer.getChannelData(1);
    224 
    225              let expected = new Float32Array(context.length);
    226 
    227              // Compute the expected output for output0 and verify the actual
    228              // output matches.
    229              expected.fill(1);
    230              for (let k = 0; k <= Math.floor(src0StartGrain); ++k) {
    231                expected[k] = 0;
    232              }
    233              for (let k = Math.ceil(src0EndGrain); k < expected.length; ++k) {
    234                expected[k] = 0;
    235              }
    236 
    237              verifyGrain(should, output0, {
    238                startGrain: src0StartGrain,
    239                endGrain: src0EndGrain,
    240                sourceName: 'src0',
    241                outputName: 'output0'
    242              });
    243 
    244              verifyGrain(should, output1, {
    245                startGrain: src1StartGrain,
    246                endGrain: src1EndGrain,
    247                sourceName: 'src1',
    248                outputName: 'output1'
    249              });
    250            })
    251            .then(() => task.done());
    252      });
    253 
    254      audit.define(
    255          'sub-sample accurate start with playbackRate', (task, should) => {
    256            // There are two channels, one for each source.  Only need to render
    257            // quanta for this test.
    258            let context = new OfflineAudioContext(
    259                {numberOfChannels: 2, length: 8192, sampleRate: sampleRate});
    260            let merger = new ChannelMergerNode(
    261                context, {numberOfInputs: context.destination.channelCount});
    262 
    263            merger.connect(context.destination);
    264 
    265            // Use a simple linear ramp for the sources with integer steps
    266            // starting at 1 to make it easy to verify and test that have
    267            // sub-sample accurate start.  Ramp starts at 1 so we can easily
    268            // tell when the source starts.
    269            let buffer = new AudioBuffer(
    270                {length: context.length, sampleRate: context.sampleRate});
    271            let r = buffer.getChannelData(0);
    272            for (let k = 0; k < r.length; ++k) {
    273              r[k] = k + 1;
    274            }
    275 
    276            // Two sources with different playback rates
    277            const src0 = new AudioBufferSourceNode(
    278                context, {buffer: buffer, playbackRate: .25});
    279            const src1 = new AudioBufferSourceNode(
    280                context, {buffer: buffer, playbackRate: 4});
    281 
    282            // Frame where sources start.  Pretty arbitrary but should not be an
    283            // integer.
    284            const startFrame = 17.8;
    285 
    286            src0.connect(merger, 0, 0);
    287            src1.connect(merger, 0, 1);
    288 
    289            src0.start(startFrame / context.sampleRate);
    290            src1.start(startFrame / context.sampleRate);
    291 
    292            context.startRendering()
    293                .then(audioBuffer => {
    294                  const output0 = audioBuffer.getChannelData(0);
    295                  const output1 = audioBuffer.getChannelData(1);
    296 
    297                  const frameBefore = Math.floor(startFrame);
    298                  const frameAfter = frameBefore + 1;
    299 
    300                  // Informative message so we know what the following output
    301                  // indices really mean.
    302                  should(startFrame, 'Source start frame')
    303                      .beEqualTo(startFrame);
    304 
    305                  // Verify the output
    306 
    307                  // With a startFrame of 17.8, the first output is at frame 18,
    308                  // but the actual start is at 17.8.  So we would interpolate
    309                  // the output 0.2 fraction of the way between 17.8 and 18, for
    310                  // an output of 1.2 for our ramp.  But the playback rate is
    311                  // 0.25, so we're really only 1/4 as far along as we think so
    312                  // the output is .2*0.25 of the way between 1 and 2 or 1.05.
    313 
    314                  const ramp0 = buffer.getChannelData(0)[0];
    315                  const ramp1 = buffer.getChannelData(0)[1];
    316 
    317                  const src0Output = ramp0 +
    318                      (ramp1 - ramp0) * (frameAfter - startFrame) *
    319                          src0.playbackRate.value;
    320 
    321                  let playbackMessage =
    322                      `With playbackRate ${src0.playbackRate.value}:`;
    323 
    324                  should(
    325                      output0[frameBefore],
    326                      `${playbackMessage} output0[${frameBefore}]`)
    327                      .beEqualTo(0);
    328                  should(
    329                      output0[frameAfter],
    330                      `${playbackMessage} output0[${frameAfter}]`)
    331                      .beCloseTo(src0Output, {threshold: 4.542e-8});
    332 
    333                  const src1Output = ramp0 +
    334                      (ramp1 - ramp0) * (frameAfter - startFrame) *
    335                          src1.playbackRate.value;
    336 
    337                  playbackMessage =
    338                      `With playbackRate ${src1.playbackRate.value}:`;
    339 
    340                  should(
    341                      output1[frameBefore],
    342                      `${playbackMessage} output1[${frameBefore}]`)
    343                      .beEqualTo(0);
    344                  should(
    345                      output1[frameAfter],
    346                      `${playbackMessage} output1[${frameAfter}]`)
    347                      .beCloseTo(src1Output, {threshold: 4.542e-8});
    348                })
    349                .then(() => task.done());
    350          });
    351 
    352      audit.run();
    353 
    354      // Given an input ramp in |rampBuffer|, interpolate the signal assuming
    355      // this ramp is used for an ABSN that starts at frame |startFrame|, which
    356      // is not necessarily an integer.  For simplicity we just use linear
    357      // interpolation here.  The interpolation is not part of the spec but
    358      // this should be pretty close to whatever interpolation is being done.
    359      function interpolateRamp(rampBuffer, startFrame) {
    360        // |start| is the last zero sample before the ABSN actually starts.
    361        const start = Math.floor(startFrame);
    362        // One less than the rampBuffer because we can't linearly interpolate
    363        // the last frame.
    364        let result = new Float32Array(rampBuffer.length - 1);
    365 
    366        for (let k = 0; k <= start; ++k) {
    367          result[k] = 0;
    368        }
    369 
    370        // Now start linear interpolation.
    371        let frame = startFrame;
    372        let index = 1;
    373        for (let k = start + 1; k < result.length; ++k) {
    374          let s0 = rampBuffer[index];
    375          let s1 = rampBuffer[index - 1];
    376          let delta = frame - k;
    377          let s = s1 - delta * (s0 - s1);
    378          result[k] = s;
    379          ++frame;
    380          ++index;
    381        }
    382 
    383        return result;
    384      }
    385 
    386      function verifyGrain(should, output, options) {
    387        let {startGrain, endGrain, sourceName, outputName} = options;
    388        let expected = new Float32Array(output.length);
    389        // Compute the expected output for output and verify the actual
    390        // output matches.
    391        expected.fill(1);
    392        for (let k = 0; k <= Math.floor(startGrain); ++k) {
    393          expected[k] = 0;
    394        }
    395        for (let k = Math.ceil(endGrain); k < expected.length; ++k) {
    396          expected[k] = 0;
    397        }
    398 
    399        should(startGrain, `${sourceName} grain start`).beEqualTo(startGrain);
    400        should(endGrain - startGrain, `${sourceName} grain duration`)
    401            .beEqualTo(endGrain - startGrain);
    402        should(endGrain, `${sourceName} grain end`).beEqualTo(endGrain);
    403        should(output, outputName).beEqualToArray(expected);
    404        should(
    405            output[Math.floor(startGrain)],
    406            `${outputName}[${Math.floor(startGrain)}]`)
    407            .beEqualTo(0);
    408        should(
    409            output[1 + Math.floor(startGrain)],
    410            `${outputName}[${1 + Math.floor(startGrain)}]`)
    411            .notBeEqualTo(0);
    412        should(
    413            output[Math.floor(endGrain)],
    414            `${outputName}[${Math.floor(endGrain)}]`)
    415            .notBeEqualTo(0);
    416        should(
    417            output[1 + Math.floor(endGrain)],
    418            `${outputName}[${1 + Math.floor(endGrain)}]`)
    419            .beEqualTo(0);
    420      }
    421    </script>
    422  </body>
    423 </html>