tor-browser

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

audioparam-testing.js (19916B)


      1 (function(global) {
      2 
      3  // Information about the starting/ending times and starting/ending values for
      4  // each time interval.
      5  let timeValueInfo;
      6 
      7  // The difference between starting values between each time interval.
      8  let startingValueDelta;
      9 
     10  // For any automation function that has an end or target value, the end value
     11  // is based the starting value of the time interval.  The starting value will
     12  // be increased or decreased by |startEndValueChange|. We choose half of
     13  // |startingValueDelta| so that the ending value will be distinct from the
     14  // starting value for next time interval.  This allows us to detect where the
     15  // ramp begins and ends.
     16  let startEndValueChange;
     17 
     18  // Default threshold to use for detecting discontinuities that should appear
     19  // at each time interval.
     20  let discontinuityThreshold;
     21 
     22  // Time interval between value changes.  It is best if 1 / numberOfTests is
     23  // not close to timeInterval.
     24  let timeIntervalInternal = .03;
     25 
     26  let context;
     27 
     28  // Make sure we render long enough to capture all of our test data.
     29  function renderLength(numberOfTests) {
     30    return timeToSampleFrame((numberOfTests + 1) * timeInterval, sampleRate);
     31  }
     32 
     33  // Create a constant reference signal with the given |value|.  Basically the
     34  // same as |createConstantBuffer|, but with the parameters to match the other
     35  // create functions.  The |endValue| is ignored.
     36  function createConstantArray(
     37      startTime, endTime, value, endValue, sampleRate) {
     38    let startFrame = timeToSampleFrame(startTime, sampleRate);
     39    let endFrame = timeToSampleFrame(endTime, sampleRate);
     40    let length = endFrame - startFrame;
     41 
     42    let buffer = createConstantBuffer(context, length, value);
     43 
     44    return buffer.getChannelData(0);
     45  }
     46 
     47  function getStartEndFrames(startTime, endTime, sampleRate) {
     48    // Start frame is the ceiling of the start time because the ramp starts at
     49    // or after the sample frame.  End frame is the ceiling because it's the
     50    // exclusive ending frame of the automation.
     51    let startFrame = Math.ceil(startTime * sampleRate);
     52    let endFrame = Math.ceil(endTime * sampleRate);
     53 
     54    return {startFrame: startFrame, endFrame: endFrame};
     55  }
     56 
     57  // Create a linear ramp starting at |startValue| and ending at |endValue|. The
     58  // ramp starts at time |startTime| and ends at |endTime|.  (The start and end
     59  // times are only used to compute how many samples to return.)
     60  function createLinearRampArray(
     61      startTime, endTime, startValue, endValue, sampleRate) {
     62    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
     63    let startFrame = frameInfo.startFrame;
     64    let endFrame = frameInfo.endFrame;
     65    let length = endFrame - startFrame;
     66    let array = new Array(length);
     67 
     68    let step = Math.fround(
     69        (endValue - startValue) / (endTime - startTime) / sampleRate);
     70    let start = Math.fround(
     71        startValue +
     72        (endValue - startValue) * (startFrame / sampleRate - startTime) /
     73            (endTime - startTime));
     74 
     75    let slope = (endValue - startValue) / (endTime - startTime);
     76 
     77    // v(t) = v0 + (v1 - v0)*(t-t0)/(t1-t0)
     78    for (k = 0; k < length; ++k) {
     79      // array[k] = Math.fround(start + k * step);
     80      let t = (startFrame + k) / sampleRate;
     81      array[k] = startValue + slope * (t - startTime);
     82    }
     83 
     84    return array;
     85  }
     86 
     87  // Create an exponential ramp starting at |startValue| and ending at
     88  // |endValue|. The ramp starts at time |startTime| and ends at |endTime|.
     89  // (The start and end times are only used to compute how many samples to
     90  // return.)
     91  function createExponentialRampArray(
     92      startTime, endTime, startValue, endValue, sampleRate) {
     93    let deltaTime = endTime - startTime;
     94 
     95    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
     96    let startFrame = frameInfo.startFrame;
     97    let endFrame = frameInfo.endFrame;
     98    let length = endFrame - startFrame;
     99    let array = new Array(length);
    100 
    101    let ratio = endValue / startValue;
    102 
    103    // v(t) = v0*(v1/v0)^((t-t0)/(t1-t0))
    104    for (let k = 0; k < length; ++k) {
    105      let t = Math.fround((startFrame + k) / sampleRate);
    106      array[k] = Math.fround(
    107          startValue * Math.pow(ratio, (t - startTime) / deltaTime));
    108    }
    109 
    110    return array;
    111  }
    112 
    113  function discreteTimeConstantForSampleRate(timeConstant, sampleRate) {
    114    return 1 - Math.exp(-1 / (sampleRate * timeConstant));
    115  }
    116 
    117  // Create a signal that starts at |startValue| and exponentially approaches
    118  // the target value of |targetValue|, using a time constant of |timeConstant|.
    119  // The ramp starts at time |startTime| and ends at |endTime|.  (The start and
    120  // end times are only used to compute how many samples to return.)
    121  function createExponentialApproachArray(
    122      startTime, endTime, startValue, targetValue, sampleRate, timeConstant) {
    123    let startFrameFloat = startTime * sampleRate;
    124    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
    125    let startFrame = frameInfo.startFrame;
    126    let endFrame = frameInfo.endFrame;
    127    let length = Math.floor(endFrame - startFrame);
    128    let array = new Array(length);
    129    let c = discreteTimeConstantForSampleRate(timeConstant, sampleRate);
    130 
    131    let delta = startValue - targetValue;
    132 
    133    // v(t) = v1 + (v0 - v1) * exp(-(t-t0)/tau)
    134    for (let k = 0; k < length; ++k) {
    135      let t = (startFrame + k) / sampleRate;
    136      let value =
    137          targetValue + delta * Math.exp(-(t - startTime) / timeConstant);
    138      array[k] = value;
    139    }
    140 
    141    return array;
    142  }
    143 
    144  // Create a sine wave of the specified duration.
    145  function createReferenceSineArray(
    146      startTime, endTime, startValue, endValue, sampleRate) {
    147    // Ignore |startValue| and |endValue| for the sine wave.
    148    let curve = createSineWaveArray(
    149        endTime - startTime, freqHz, sineAmplitude, sampleRate);
    150    // Sample the curve appropriately.
    151    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
    152    let startFrame = frameInfo.startFrame;
    153    let endFrame = frameInfo.endFrame;
    154    let length = Math.floor(endFrame - startFrame);
    155    let array = new Array(length);
    156 
    157    // v(t) = linearly interpolate between V[k] and V[k + 1] where k =
    158    // floor((N-1)/duration*(t - t0))
    159    let f = (length - 1) / (endTime - startTime);
    160 
    161    for (let k = 0; k < length; ++k) {
    162      let t = (startFrame + k) / sampleRate;
    163      let indexFloat = f * (t - startTime);
    164      let index = Math.floor(indexFloat);
    165      if (index + 1 < length) {
    166        let v0 = curve[index];
    167        let v1 = curve[index + 1];
    168        array[k] = v0 + (v1 - v0) * (indexFloat - index);
    169      } else {
    170        array[k] = curve[length - 1];
    171      }
    172    }
    173 
    174    return array;
    175  }
    176 
    177  // Create a sine wave of the given frequency and amplitude.  The sine wave is
    178  // offset by half the amplitude so that result is always positive.
    179  function createSineWaveArray(durationSeconds, freqHz, amplitude, sampleRate) {
    180    let length = timeToSampleFrame(durationSeconds, sampleRate);
    181    let signal = new Float32Array(length);
    182    let omega = 2 * Math.PI * freqHz / sampleRate;
    183    let halfAmplitude = amplitude / 2;
    184 
    185    for (let k = 0; k < length; ++k) {
    186      signal[k] = halfAmplitude + halfAmplitude * Math.sin(omega * k);
    187    }
    188 
    189    return signal;
    190  }
    191 
    192  // Return the difference between the starting value and the ending value for
    193  // time interval |timeIntervalIndex|.  We alternate between an end value that
    194  // is above or below the starting value.
    195  function endValueDelta(timeIntervalIndex) {
    196    if (timeIntervalIndex & 1) {
    197      return -startEndValueChange;
    198    } else {
    199      return startEndValueChange;
    200    }
    201  }
    202 
    203  // Relative error metric
    204  function relativeErrorMetric(actual, expected) {
    205    return (actual - expected) / Math.abs(expected);
    206  }
    207 
    208  // Difference metric
    209  function differenceErrorMetric(actual, expected) {
    210    return actual - expected;
    211  }
    212 
    213  // Return the difference between the starting value at |timeIntervalIndex| and
    214  // the starting value at the next time interval.  Since we started at a large
    215  // initial value, we decrease the value at each time interval.
    216  function valueUpdate(timeIntervalIndex) {
    217    return -startingValueDelta;
    218  }
    219 
    220  // Compare a section of the rendered data against our expected signal.
    221  function comparePartialSignals(
    222      rendered, expectedFunction, startTime, endTime, valueInfo,
    223      sampleRateParam, errorMetric) {
    224    let startSample = timeToSampleFrame(startTime, sampleRateParam);
    225    let expected = expectedFunction(
    226        startTime, endTime, valueInfo.startValue, valueInfo.endValue,
    227        sampleRateParam, timeConstant);
    228 
    229    let n = expected.length;
    230    let maxError = -1;
    231    let maxErrorIndex = -1;
    232 
    233    for (let k = 0; k < n; ++k) {
    234      // Make sure we don't pass these tests because a NaN has been generated in
    235      // either the
    236      // rendered data or the reference data.
    237      if (!isValidNumber(rendered[startSample + k])) {
    238        maxError = Infinity;
    239        maxErrorIndex = startSample + k;
    240        assert_true(
    241            isValidNumber(rendered[startSample + k]),
    242            `NaN or infinity for rendered data at ${maxErrorIndex}`);
    243        break;
    244      }
    245      if (!isValidNumber(expected[k])) {
    246        maxError = Infinity;
    247        maxErrorIndex = startSample + k;
    248        assert_true(
    249            isValidNumber(expected[k]),
    250            `NaN or infinity for rendered data at ${maxErrorIndex}`);
    251        break;
    252      }
    253      let error = Math.abs(errorMetric(rendered[startSample + k], expected[k]));
    254      if (error > maxError) {
    255        maxError = error;
    256        maxErrorIndex = k;
    257      }
    258    }
    259 
    260    return {maxError: maxError, index: maxErrorIndex, expected: expected};
    261  };
    262 
    263  // Find the discontinuities in the data and compare the locations of the
    264  // discontinuities with the times that define the time intervals. There is a
    265  // discontinuity if the difference between successive samples exceeds the
    266  // threshold.
    267  function verifyDiscontinuities(values, times, threshold) {
    268    let n = values.length;
    269    let success = true;
    270    let badLocations = 0;
    271    let breaks = [];
    272 
    273    // Find discontinuities.
    274    for (let k = 1; k < n; ++k) {
    275      if (Math.abs(values[k] - values[k - 1]) > threshold) {
    276        breaks.push(k);
    277      }
    278    }
    279 
    280    let testCount;
    281 
    282    // If there are numberOfTests intervals, there are only numberOfTests - 1
    283    // internal interval boundaries. Hence the maximum number of discontinuties
    284    // we expect to find is numberOfTests - 1. If we find more than that, we
    285    // have no reference to compare against. We also assume that the actual
    286    // discontinuities are close to the expected ones.
    287    //
    288    // This is just a sanity check when something goes really wrong.  For
    289    // example, if the threshold is too low, every sample frame looks like a
    290    // discontinuity.
    291    if (breaks.length >= numberOfTests) {
    292      testCount = numberOfTests - 1;
    293      assert_less_than(
    294          breaks.length, numberOfTests, 'Number of discontinuities');
    295      success = false;
    296    } else {
    297      testCount = breaks.length;
    298    }
    299 
    300    // Compare the location of each discontinuity with the end time of each
    301    // interval. (There is no discontinuity at the start of the signal.)
    302    for (let k = 0; k < testCount; ++k) {
    303      let expectedSampleFrame = timeToSampleFrame(times[k + 1], sampleRate);
    304      if (breaks[k] != expectedSampleFrame) {
    305        success = false;
    306        ++badLocations;
    307        assert_equals(breaks[k], expectedSampleFrame, 'Discontinuity at index');
    308      }
    309    }
    310 
    311    if (badLocations) {
    312      assert_equals(
    313          badLocations, 0, 'Number of discontinuites at incorrect locations');
    314      success = false;
    315    } else {
    316      assert_equals(
    317          breaks.length + 1,
    318          numberOfTests,
    319          'Number of tests started and ended at the correct time');
    320    }
    321 
    322    return success;
    323  };
    324 
    325  // Compare the rendered data with the expected data.
    326  //
    327  // testName - string describing the test
    328  //
    329  // maxError - maximum allowed difference between the rendered data and the
    330  // expected data
    331  //
    332  // rendererdData - array containing the rendered (actual) data
    333  //
    334  // expectedFunction - function to compute the expected data
    335  //
    336  // timeValueInfo - array containing information about the start and end times
    337  // and the start and end values of each interval.
    338  //
    339  // breakThreshold - threshold to use for determining discontinuities.
    340  function compareSignals(
    341      testName, maxError, renderedData, expectedFunction, timeValueInfo,
    342      breakThreshold, errorMetric) {
    343    let success = true;
    344    const failedTestCount = 0;
    345    const times = timeValueInfo.times;
    346    const values = timeValueInfo.values;
    347    const n = values.length;
    348    let expectedSignal = [];
    349 
    350    success =
    351        verifyDiscontinuities(renderedData, times, breakThreshold);
    352 
    353    for (let k = 0; k < n; ++k) {
    354      const result = comparePartialSignals(
    355          renderedData, expectedFunction, times[k], times[k + 1],
    356          values[k], sampleRate, errorMetric);
    357 
    358      expectedSignal =
    359          expectedSignal.concat(Array.prototype.slice.call(result.expected));
    360 
    361      assert_less_than_equal(
    362          result.maxError,
    363          maxError,
    364          `Max error for test ${k} at offset ` +
    365              `${result.index + timeToSampleFrame(times[k], sampleRate)}`);
    366    }
    367 
    368    assert_equals(
    369        failedTestCount,
    370        0,
    371        `Number of failed tests with an acceptable relative ` +
    372            `tolerance of ${maxError}`);
    373  };
    374 
    375  // Create a function to test the rendered data with the reference data.
    376  //
    377  // testName - string describing the test
    378  //
    379  // error - max allowed error between rendered data and the reference data.
    380  //
    381  // referenceFunction - function that generates the reference data to be
    382  // compared with the rendered data.
    383  //
    384  // jumpThreshold - optional parameter that specifies the threshold to use for
    385  // detecting discontinuities.  If not specified, defaults to
    386  // discontinuityThreshold.
    387  //
    388  function checkResultFunction(
    389      task, testName, error, referenceFunction, jumpThreshold,
    390      errorMetric) {
    391    return function(event) {
    392      let buffer = event.renderedBuffer;
    393      renderedData = buffer.getChannelData(0);
    394 
    395      let threshold;
    396 
    397      if (!jumpThreshold) {
    398        threshold = discontinuityThreshold;
    399      } else {
    400        threshold = jumpThreshold;
    401      }
    402 
    403      compareSignals(
    404          testName, error, renderedData, referenceFunction,
    405          timeValueInfo, threshold, errorMetric);
    406      task.done();
    407    }
    408  }
    409 
    410  // Run all the automation tests.
    411  //
    412  // numberOfTests - number of tests (time intervals) to run.
    413  //
    414  // initialValue - The initial value of the first time interval.
    415  //
    416  // setValueFunction - function that sets the specified value at the start of a
    417  // time interval.
    418  //
    419  // automationFunction - function that sets the end value for the time
    420  // interval. It specifies how the value approaches the end value.
    421  //
    422  // An object is returned containing an array of start times for each time
    423  // interval, and an array giving the start and end values for the interval.
    424  function doAutomation(
    425      numberOfTests, initialValue, setValueFunction, automationFunction) {
    426    let timeInfo = [0];
    427    let valueInfo = [];
    428    let value = initialValue;
    429 
    430    for (let k = 0; k < numberOfTests; ++k) {
    431      let startTime = k * timeInterval;
    432      let endTime = (k + 1) * timeInterval;
    433      let endValue = value + endValueDelta(k);
    434 
    435      // Set the value at the start of the time interval.
    436      setValueFunction(value, startTime);
    437 
    438      // Specify the end or target value, and how we should approach it.
    439      automationFunction(endValue, startTime, endTime);
    440 
    441      // Keep track of the start times, and the start and end values for each
    442      // time interval.
    443      timeInfo.push(endTime);
    444      valueInfo.push({startValue: value, endValue: endValue});
    445 
    446      value += valueUpdate(k);
    447    }
    448 
    449    return {times: timeInfo, values: valueInfo};
    450  }
    451 
    452  // Create the audio graph for the test and then run the test.
    453  //
    454  // numberOfTests - number of time intervals (tests) to run.
    455  //
    456  // initialValue - the initial value of the gain at time 0.
    457  //
    458  // setValueFunction - function to set the value at the beginning of each time
    459  // interval.
    460  //
    461  // automationFunction - the AudioParamTimeline automation function
    462  //
    463  // testName - string indicating the test that is being run.
    464  //
    465  // maxError - maximum allowed error between the rendered data and the
    466  // reference data
    467  //
    468  // referenceFunction - function that generates the reference data to be
    469  // compared against the rendered data.
    470  //
    471  // jumpThreshold - optional parameter that specifies the threshold to use for
    472  // detecting discontinuities.  If not specified, defaults to
    473  // discontinuityThreshold.
    474  function createAudioGraphAndTest(
    475      task, should, numberOfTests, initialValue, setValueFunction,
    476      automationFunction, testName, maxError, referenceFunction, jumpThreshold,
    477      errorMetric) {
    478    // Create offline audio context.
    479    context =
    480        new OfflineAudioContext(2, renderLength(numberOfTests), sampleRate);
    481    let constantBuffer =
    482        createConstantBuffer(context, renderLength(numberOfTests), 1);
    483 
    484    // We use an AudioGainNode here simply as a convenient way to test the
    485    // AudioParam automation, since it's easy to pass a constant value through
    486    // the node, automate the .gain attribute and observe the resulting values.
    487 
    488    gainNode = context.createGain();
    489 
    490    let bufferSource = context.createBufferSource();
    491    bufferSource.buffer = constantBuffer;
    492    bufferSource.connect(gainNode);
    493    gainNode.connect(context.destination);
    494 
    495    // Set up default values for the parameters that control how the automation
    496    // test values progress for each time interval.
    497    startingValueDelta = initialValue / numberOfTests;
    498    startEndValueChange = startingValueDelta / 2;
    499    discontinuityThreshold = startEndValueChange / 2;
    500 
    501    // Run the automation tests.
    502    timeValueInfo = doAutomation(
    503        numberOfTests, initialValue, setValueFunction, automationFunction);
    504    bufferSource.start(0);
    505 
    506    context.oncomplete = checkResultFunction(
    507        task, testName, maxError, referenceFunction, jumpThreshold,
    508        errorMetric || relativeErrorMetric);
    509    context.startRendering();
    510  }
    511 
    512  // Export local references to global scope. All the new objects in this file
    513  // must be exported through this if it is to be used in the actual test HTML
    514  // page.
    515  let exports = {
    516    'sampleRate': 44100,
    517    'gainNode': null,
    518    'timeInterval': timeIntervalInternal,
    519 
    520    // Some suitable time constant so that we can see a significant change over
    521    // a timeInterval.  This is only needed by setTargetAtTime() which needs a
    522    // time constant.
    523    'timeConstant': timeIntervalInternal / 3,
    524 
    525    'renderLength': renderLength,
    526    'createConstantArray': createConstantArray,
    527    'getStartEndFrames': getStartEndFrames,
    528    'createLinearRampArray': createLinearRampArray,
    529    'createExponentialRampArray': createExponentialRampArray,
    530    'discreteTimeConstantForSampleRate': discreteTimeConstantForSampleRate,
    531    'createExponentialApproachArray': createExponentialApproachArray,
    532    'createReferenceSineArray': createReferenceSineArray,
    533    'createSineWaveArray': createSineWaveArray,
    534    'endValueDelta': endValueDelta,
    535    'relativeErrorMetric': relativeErrorMetric,
    536    'differenceErrorMetric': differenceErrorMetric,
    537    'valueUpdate': valueUpdate,
    538    'comparePartialSignals': comparePartialSignals,
    539    'verifyDiscontinuities': verifyDiscontinuities,
    540    'compareSignals': compareSignals,
    541    'checkResultFunction': checkResultFunction,
    542    'doAutomation': doAutomation,
    543    'createAudioGraphAndTest': createAudioGraphAndTest
    544  };
    545 
    546  for (let reference in exports) {
    547    global[reference] = exports[reference];
    548  }
    549 
    550 })(window);