tor-browser

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

k-rate-biquad-connection.html (18235B)


      1 <!doctype html>
      2 <html>
      3  <head>
      4    <title>Test k-rate AudioParam Inputs for BiquadFilterNode</title>
      5    <script src="/resources/testharness.js"></script>
      6    <script src="/resources/testharnessreport.js"></script>
      7    <script src="/webaudio/resources/audit-util.js"></script>
      8    <script src="/webaudio/resources/audit.js"></script>
      9  </head>
     10 
     11  <body>
     12    <script>
     13      // sampleRate and duration are fairly arbitrary.  We use low values to
     14      // limit the complexity of the test.
     15      let sampleRate = 8192;
     16      let testDuration = 0.5;
     17 
     18      let audit = Audit.createTaskRunner();
     19 
     20      audit.define(
     21          {label: 'Frequency AudioParam', description: 'k-rate input works'},
     22          async (task, should) => {
     23            // Test frequency AudioParam using a lowpass filter whose bandwidth
     24            // is initially larger than the oscillator frequency.  Then automate
     25            // the frequency to 0 so that the output of the filter is 0 (because
     26            // the cutoff is 0).
     27            let oscFrequency = 440;
     28 
     29            let options = {
     30              sampleRate: sampleRate,
     31              paramName: 'frequency',
     32              oscFrequency: oscFrequency,
     33              testDuration: testDuration,
     34              filterOptions: {type: 'lowpass', frequency: 0},
     35              autoStart:
     36                  {method: 'setValueAtTime', args: [2 * oscFrequency, 0]},
     37              autoEnd: {
     38                method: 'linearRampToValueAtTime',
     39                args: [0, testDuration / 4]
     40              }
     41            };
     42 
     43            let buffer = await doTest(should, options);
     44            let expected = buffer.getChannelData(0);
     45            let actual = buffer.getChannelData(1);
     46            let halfLength = expected.length / 2;
     47 
     48            // Sanity check.  The expected output should not be zero for
     49            // the first half, but should be zero for the second half
     50            // (because the filter bandwidth is exactly 0).
     51            const prefix = 'Expected k-rate frequency with automation';
     52 
     53            should(
     54                expected.slice(0, halfLength),
     55                `${prefix} output[0:${halfLength - 1}]`)
     56                .notBeConstantValueOf(0);
     57            should(
     58                expected.slice(expected.length),
     59                `${prefix} output[${halfLength}:]`)
     60                .beConstantValueOf(0);
     61 
     62            // Outputs should be the same.  Break the message into two
     63            // parts so we can see the expected outputs.
     64            checkForSameOutput(should, options.paramName, actual, expected);
     65 
     66            task.done();
     67          });
     68 
     69      audit.define(
     70          {label: 'Q AudioParam', description: 'k-rate input works'},
     71          async (task, should) => {
     72            // Test Q AudioParam.  Use a bandpass filter whose center frequency
     73            // is fairly far from the oscillator frequency.  Then start with a Q
     74            // value of 0 (so everything goes through) and then increase Q to
     75            // some large value such that the out-of-band signals are basically
     76            // cutoff.
     77            let frequency = 440;
     78            let oscFrequency = 4 * frequency;
     79 
     80            let options = {
     81              sampleRate: sampleRate,
     82              oscFrequency: oscFrequency,
     83              testDuration: testDuration,
     84              paramName: 'Q',
     85              filterOptions: {type: 'bandpass', frequency: frequency, Q: 0},
     86              autoStart: {method: 'setValueAtTime', args: [0, 0]},
     87              autoEnd: {
     88                method: 'linearRampToValueAtTime',
     89                args: [100, testDuration / 4]
     90              }
     91            };
     92 
     93            const buffer = await doTest(should, options);
     94            let expected = buffer.getChannelData(0);
     95            let actual = buffer.getChannelData(1);
     96 
     97            // Outputs should be the same
     98            checkForSameOutput(should, options.paramName, actual, expected);
     99 
    100            task.done();
    101          });
    102 
    103      audit.define(
    104          {label: 'Gain AudioParam', description: 'k-rate input works'},
    105          async (task, should) => {
    106            // Test gain AudioParam.  Use a peaking filter with a large Q so the
    107            // peak is narrow with a center frequency the same as the oscillator
    108            // frequency.  Start with a gain of 0 so everything goes through and
    109            // then ramp the gain down to -100 so that the oscillator is
    110            // filtered out.
    111            let oscFrequency = 4 * 440;
    112 
    113            let options = {
    114              sampleRate: sampleRate,
    115              oscFrequency: oscFrequency,
    116              testDuration: testDuration,
    117              paramName: 'gain',
    118              filterOptions:
    119                  {type: 'peaking', frequency: oscFrequency, Q: 100, gain: 0},
    120              autoStart: {method: 'setValueAtTime', args: [0, 0]},
    121              autoEnd: {
    122                method: 'linearRampToValueAtTime',
    123                args: [-100, testDuration / 4]
    124              }
    125            };
    126 
    127            const buffer = await doTest(should, options);
    128            let expected = buffer.getChannelData(0);
    129            let actual = buffer.getChannelData(1);
    130 
    131            // Outputs should be the same
    132            checkForSameOutput(should, options.paramName, actual, expected);
    133 
    134            task.done();
    135          });
    136 
    137      audit.define(
    138          {label: 'Detune AudioParam', description: 'k-rate input works'},
    139          async (task, should) => {
    140            // Test detune AudioParam.  The basic idea is the same as the
    141            // frequency test above, but insteda of automating the frequency, we
    142            // automate the detune value so that initially the filter cutuff is
    143            // unchanged and then changing the detune until the cutoff goes to 1
    144            // Hz, which would cause the oscillator to be filtered out.
    145            let oscFrequency = 440;
    146            let filterFrequency = 5 * oscFrequency;
    147 
    148            // For a detune value d, the computed frequency, fc, of the filter
    149            // is fc = f*2^(d/1200), where f is the frequency of the filter.  Or
    150            // d = 1200*log2(fc/f).  Compute the detune value to produce a final
    151            // cutoff frequency of 1 Hz.
    152            let detuneEnd = 1200 * Math.log2(1 / filterFrequency);
    153 
    154            let options = {
    155              sampleRate: sampleRate,
    156              oscFrequency: oscFrequency,
    157              testDuration: testDuration,
    158              paramName: 'detune',
    159              filterOptions: {
    160                type: 'lowpass',
    161                frequency: filterFrequency,
    162                detune: 0,
    163                gain: 0
    164              },
    165              autoStart: {method: 'setValueAtTime', args: [0, 0]},
    166              autoEnd: {
    167                method: 'linearRampToValueAtTime',
    168                args: [detuneEnd, testDuration / 4]
    169              }
    170            };
    171 
    172            const buffer = await doTest(should, options);
    173            let expected = buffer.getChannelData(0);
    174            let actual = buffer.getChannelData(1);
    175 
    176            // Outputs should be the same
    177            checkForSameOutput(should, options.paramName, actual, expected);
    178 
    179            task.done();
    180          });
    181 
    182      audit.define('All k-rate inputs', async (task, should) => {
    183        // Test the case where all AudioParams are set to k-rate with an input
    184        // to each AudioParam.  Similar to the above tests except all the params
    185        // are k-rate.
    186        let testFrames = testDuration * sampleRate;
    187        let context = new OfflineAudioContext(
    188            {numberOfChannels: 2, sampleRate: sampleRate, length: testFrames});
    189 
    190        let merger = new ChannelMergerNode(
    191            context, {numberOfInputs: context.destination.channelCount});
    192        merger.connect(context.destination);
    193 
    194        let src = new OscillatorNode(context);
    195 
    196        // The peaking filter uses all four AudioParams, so this is the node to
    197        // test.
    198        let filterOptions =
    199            {type: 'peaking', frequency: 0, detune: 0, gain: 0, Q: 0};
    200        let refNode;
    201        should(
    202            () => refNode = new BiquadFilterNode(context, filterOptions),
    203            `Create: refNode = new BiquadFilterNode(context, ${
    204                JSON.stringify(filterOptions)})`)
    205            .notThrow();
    206 
    207        let tstNode;
    208        should(
    209            () => tstNode = new BiquadFilterNode(context, filterOptions),
    210            `Create: tstNode = new BiquadFilterNode(context, ${
    211                JSON.stringify(filterOptions)})`)
    212            .notThrow();
    213        ;
    214 
    215        // Make all the AudioParams k-rate.
    216        ['frequency', 'Q', 'gain', 'detune'].forEach(param => {
    217          should(
    218              () => refNode[param].automationRate = 'k-rate',
    219              `Set rate: refNode[${param}].automationRate = 'k-rate'`)
    220              .notThrow();
    221          should(
    222              () => tstNode[param].automationRate = 'k-rate',
    223              `Set rate: tstNode[${param}].automationRate = 'k-rate'`)
    224              .notThrow();
    225        });
    226 
    227        // One input for each AudioParam.
    228        let mod = {};
    229        ['frequency', 'Q', 'gain', 'detune'].forEach(param => {
    230          should(
    231              () => mod[param] = new ConstantSourceNode(context, {offset: 0}),
    232              `Create: mod[${
    233                  param}] = new ConstantSourceNode(context, {offset: 0})`)
    234              .notThrow();
    235          ;
    236          should(
    237              () => mod[param].offset.automationRate = 'a-rate',
    238              `Set rate: mod[${param}].offset.automationRate = 'a-rate'`)
    239              .notThrow();
    240        });
    241 
    242        // Set up automations for refNode.  We want to start the filter with
    243        // parameters that let the oscillator signal through more or less
    244        // untouched.  Then change the filter parameters to filter out the
    245        // oscillator.  What happens in between doesn't reall matter for this
    246        // test.  Hence, set the initial parameters with a center frequency well
    247        // above the oscillator and a Q and gain of 0 to pass everthing.
    248        [['frequency', [4 * src.frequency.value, 0]], ['Q', [0, 0]],
    249         ['gain', [0, 0]], ['detune', [4 * 1200, 0]]]
    250            .forEach(param => {
    251              should(
    252                  () => refNode[param[0]].setValueAtTime(...param[1]),
    253                  `Automate 0: refNode.${param[0]}.setValueAtTime(${
    254                      param[1][0]}, ${param[1][1]})`)
    255                  .notThrow();
    256              should(
    257                  () => mod[param[0]].offset.setValueAtTime(...param[1]),
    258                  `Automate 0: mod[${param[0]}].offset.setValueAtTime(${
    259                      param[1][0]}, ${param[1][1]})`)
    260                  .notThrow();
    261            });
    262 
    263        // Now move the filter frequency to the oscillator frequency with a high
    264        // Q and very low gain to remove the oscillator signal.
    265        [['frequency', [src.frequency.value, testDuration / 4]],
    266         ['Q', [40, testDuration / 4]], ['gain', [-100, testDuration / 4]], [
    267           'detune', [0, testDuration / 4]
    268         ]].forEach(param => {
    269          should(
    270              () => refNode[param[0]].linearRampToValueAtTime(...param[1]),
    271              `Automate 1: refNode[${param[0]}].linearRampToValueAtTime(${
    272                  param[1][0]}, ${param[1][1]})`)
    273              .notThrow();
    274          should(
    275              () => mod[param[0]].offset.linearRampToValueAtTime(...param[1]),
    276              `Automate 1: mod[${param[0]}].offset.linearRampToValueAtTime(${
    277                  param[1][0]}, ${param[1][1]})`)
    278              .notThrow();
    279        });
    280 
    281        // Connect everything
    282        src.connect(refNode).connect(merger, 0, 0);
    283        src.connect(tstNode).connect(merger, 0, 1);
    284 
    285        src.start();
    286        for (let param in mod) {
    287          should(
    288              () => mod[param].connect(tstNode[param]),
    289              `Connect: mod[${param}].connect(tstNode.${param})`)
    290              .notThrow();
    291        }
    292 
    293        for (let param in mod) {
    294          should(() => mod[param].start(), `Start: mod[${param}].start()`)
    295              .notThrow();
    296        }
    297 
    298        const buffer = await context.startRendering();
    299        let expected = buffer.getChannelData(0);
    300        let actual = buffer.getChannelData(1);
    301 
    302        // Sanity check that the output isn't all zeroes.
    303        should(actual, 'All k-rate AudioParams').notBeConstantValueOf(0);
    304        should(actual, 'All k-rate AudioParams').beCloseToArray(expected, {
    305          absoluteThreshold: 0
    306        });
    307 
    308        task.done();
    309      });
    310 
    311      audit.run();
    312 
    313      async function doTest(should, options) {
    314        // Test that a k-rate AudioParam with an input reads the input value and
    315        // is actually k-rate.
    316        //
    317        // A refNode is created with an automation timeline.  This is the
    318        // expected output.
    319        //
    320        // The testNode is the same, but it has a node connected to the k-rate
    321        // AudioParam.  The input to the node is an a-rate ConstantSourceNode
    322        // whose output is automated in exactly the same was as the refNode.  If
    323        // the test passes, the outputs of the two nodes MUST match exactly.
    324 
    325        // The options argument MUST contain the following members:
    326        //   sampleRate - the sample rate for the offline context
    327        //   testDuration - duration of the offline context, in sec.
    328        //   paramName  - the name of the AudioParam to be tested
    329        //   oscFrequency - frequency of oscillator source
    330        //   filterOptions - options used to construct the BiquadFilterNode
    331        //   autoStart     - information about how to start the automation
    332        //   autoEnd       - information about how to end the automation
    333        //
    334        //   The autoStart and autoEnd options are themselves dictionaries with
    335        //   the following required members:
    336        //     method - name of the automation method to be applied
    337        //     args   - array of arguments to be supplied to the method.
    338        let {
    339          sampleRate,
    340          paramName,
    341          oscFrequency,
    342          autoStart,
    343          autoEnd,
    344          testDuration,
    345          filterOptions
    346        } = options;
    347 
    348        let testFrames = testDuration * sampleRate;
    349        let context = new OfflineAudioContext(
    350            {numberOfChannels: 2, sampleRate: sampleRate, length: testFrames});
    351 
    352        let merger = new ChannelMergerNode(
    353            context, {numberOfInputs: context.destination.channelCount});
    354        merger.connect(context.destination);
    355 
    356        // Any calls to |should| are meant to be informational so we can see
    357        // what nodes are created and the automations used.
    358        let src;
    359 
    360        // Create the source.
    361        should(
    362            () => {
    363              src = new OscillatorNode(context, {frequency: oscFrequency});
    364            },
    365            `${paramName}: new OscillatorNode(context, {frequency: ${
    366                oscFrequency}})`)
    367            .notThrow();
    368 
    369        // The refNode automates the AudioParam with k-rate automations, no
    370        // inputs.
    371        let refNode;
    372        should(
    373            () => {
    374              refNode = new BiquadFilterNode(context, filterOptions);
    375            },
    376            `Reference BiquadFilterNode(c, ${JSON.stringify(filterOptions)})`)
    377            .notThrow();
    378 
    379        refNode[paramName].automationRate = 'k-rate';
    380 
    381        // Set up automations for the reference node.
    382        should(
    383            () => {
    384              refNode[paramName][autoStart.method](...autoStart.args);
    385            },
    386            `refNode.${paramName}.${autoStart.method}(${autoStart.args})`)
    387            .notThrow();
    388        should(
    389            () => {
    390              refNode[paramName][autoEnd.method](...autoEnd.args);
    391            },
    392            `refNode.${paramName}.${autoEnd.method}.(${autoEnd.args})`)
    393            .notThrow();
    394 
    395        // The tstNode does the same automation, but it comes from the input
    396        // connected to the AudioParam.
    397        let tstNode;
    398        should(
    399            () => {
    400              tstNode = new BiquadFilterNode(context, filterOptions);
    401            },
    402            `Test BiquadFilterNode(context, ${JSON.stringify(filterOptions)})`)
    403            .notThrow();
    404        tstNode[paramName].automationRate = 'k-rate';
    405 
    406        // Create the input to the AudioParam of the test node.  The output of
    407        // this node MUST have the same set of automations as the reference
    408        // node, and MUST be a-rate to make sure we're handling k-rate inputs
    409        // correctly.
    410        let mod = new ConstantSourceNode(context);
    411        mod.offset.automationRate = 'a-rate';
    412        should(
    413            () => {
    414              mod.offset[autoStart.method](...autoStart.args);
    415            },
    416            `${paramName}: mod.offset.${autoStart.method}(${autoStart.args})`)
    417            .notThrow();
    418        should(
    419            () => {
    420              mod.offset[autoEnd.method](...autoEnd.args);
    421            },
    422            `${paramName}: mod.offset.${autoEnd.method}(${autoEnd.args})`)
    423            .notThrow();
    424 
    425        // Create graph
    426        mod.connect(tstNode[paramName]);
    427        src.connect(refNode).connect(merger, 0, 0);
    428        src.connect(tstNode).connect(merger, 0, 1);
    429 
    430        // Run!
    431        src.start();
    432        mod.start();
    433        return context.startRendering();
    434      }
    435 
    436      function checkForSameOutput(should, paramName, actual, expected) {
    437        let halfLength = expected.length / 2;
    438 
    439        // Outputs should be the same.  We break the check into halves so we can
    440        // see the expected outputs.  Mostly for a simple visual check that the
    441        // output from the second half is small because the tests generally try
    442        // to filter out the signal so that the last half of the output is
    443        // small.
    444        should(
    445            actual.slice(0, halfLength),
    446            `k-rate ${paramName} with input: output[0,${halfLength}]`)
    447            .beCloseToArray(
    448                expected.slice(0, halfLength), {absoluteThreshold: 0});
    449        should(
    450            actual.slice(halfLength),
    451            `k-rate ${paramName} with input: output[${halfLength}:]`)
    452            .beCloseToArray(expected.slice(halfLength), {absoluteThreshold: 0});
    453      }
    454    </script>
    455  </body>
    456 </html>