summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/resources/audioparam-testing.js
blob: bc90ddbef828510213d95cf40683e23237e56633 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
(function(global) {

  // Information about the starting/ending times and starting/ending values for
  // each time interval.
  let timeValueInfo;

  // The difference between starting values between each time interval.
  let startingValueDelta;

  // For any automation function that has an end or target value, the end value
  // is based the starting value of the time interval.  The starting value will
  // be increased or decreased by |startEndValueChange|. We choose half of
  // |startingValueDelta| so that the ending value will be distinct from the
  // starting value for next time interval.  This allows us to detect where the
  // ramp begins and ends.
  let startEndValueChange;

  // Default threshold to use for detecting discontinuities that should appear
  // at each time interval.
  let discontinuityThreshold;

  // Time interval between value changes.  It is best if 1 / numberOfTests is
  // not close to timeInterval.
  let timeIntervalInternal = .03;

  let context;

  // Make sure we render long enough to capture all of our test data.
  function renderLength(numberOfTests) {
    return timeToSampleFrame((numberOfTests + 1) * timeInterval, sampleRate);
  }

  // Create a constant reference signal with the given |value|.  Basically the
  // same as |createConstantBuffer|, but with the parameters to match the other
  // create functions.  The |endValue| is ignored.
  function createConstantArray(
      startTime, endTime, value, endValue, sampleRate) {
    let startFrame = timeToSampleFrame(startTime, sampleRate);
    let endFrame = timeToSampleFrame(endTime, sampleRate);
    let length = endFrame - startFrame;

    let buffer = createConstantBuffer(context, length, value);

    return buffer.getChannelData(0);
  }

  function getStartEndFrames(startTime, endTime, sampleRate) {
    // Start frame is the ceiling of the start time because the ramp starts at
    // or after the sample frame.  End frame is the ceiling because it's the
    // exclusive ending frame of the automation.
    let startFrame = Math.ceil(startTime * sampleRate);
    let endFrame = Math.ceil(endTime * sampleRate);

    return {startFrame: startFrame, endFrame: endFrame};
  }

  // Create a linear ramp starting at |startValue| and ending at |endValue|. The
  // ramp starts at time |startTime| and ends at |endTime|.  (The start and end
  // times are only used to compute how many samples to return.)
  function createLinearRampArray(
      startTime, endTime, startValue, endValue, sampleRate) {
    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
    let startFrame = frameInfo.startFrame;
    let endFrame = frameInfo.endFrame;
    let length = endFrame - startFrame;
    let array = new Array(length);

    let step = Math.fround(
        (endValue - startValue) / (endTime - startTime) / sampleRate);
    let start = Math.fround(
        startValue +
        (endValue - startValue) * (startFrame / sampleRate - startTime) /
            (endTime - startTime));

    let slope = (endValue - startValue) / (endTime - startTime);

    // v(t) = v0 + (v1 - v0)*(t-t0)/(t1-t0)
    for (k = 0; k < length; ++k) {
      // array[k] = Math.fround(start + k * step);
      let t = (startFrame + k) / sampleRate;
      array[k] = startValue + slope * (t - startTime);
    }

    return array;
  }

  // Create an exponential ramp starting at |startValue| and ending at
  // |endValue|. The ramp starts at time |startTime| and ends at |endTime|.
  // (The start and end times are only used to compute how many samples to
  // return.)
  function createExponentialRampArray(
      startTime, endTime, startValue, endValue, sampleRate) {
    let deltaTime = endTime - startTime;

    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
    let startFrame = frameInfo.startFrame;
    let endFrame = frameInfo.endFrame;
    let length = endFrame - startFrame;
    let array = new Array(length);

    let ratio = endValue / startValue;

    // v(t) = v0*(v1/v0)^((t-t0)/(t1-t0))
    for (let k = 0; k < length; ++k) {
      let t = Math.fround((startFrame + k) / sampleRate);
      array[k] = Math.fround(
          startValue * Math.pow(ratio, (t - startTime) / deltaTime));
    }

    return array;
  }

  function discreteTimeConstantForSampleRate(timeConstant, sampleRate) {
    return 1 - Math.exp(-1 / (sampleRate * timeConstant));
  }

  // Create a signal that starts at |startValue| and exponentially approaches
  // the target value of |targetValue|, using a time constant of |timeConstant|.
  // The ramp starts at time |startTime| and ends at |endTime|.  (The start and
  // end times are only used to compute how many samples to return.)
  function createExponentialApproachArray(
      startTime, endTime, startValue, targetValue, sampleRate, timeConstant) {
    let startFrameFloat = startTime * sampleRate;
    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
    let startFrame = frameInfo.startFrame;
    let endFrame = frameInfo.endFrame;
    let length = Math.floor(endFrame - startFrame);
    let array = new Array(length);
    let c = discreteTimeConstantForSampleRate(timeConstant, sampleRate);

    let delta = startValue - targetValue;

    // v(t) = v1 + (v0 - v1) * exp(-(t-t0)/tau)
    for (let k = 0; k < length; ++k) {
      let t = (startFrame + k) / sampleRate;
      let value =
          targetValue + delta * Math.exp(-(t - startTime) / timeConstant);
      array[k] = value;
    }

    return array;
  }

  // Create a sine wave of the specified duration.
  function createReferenceSineArray(
      startTime, endTime, startValue, endValue, sampleRate) {
    // Ignore |startValue| and |endValue| for the sine wave.
    let curve = createSineWaveArray(
        endTime - startTime, freqHz, sineAmplitude, sampleRate);
    // Sample the curve appropriately.
    let frameInfo = getStartEndFrames(startTime, endTime, sampleRate);
    let startFrame = frameInfo.startFrame;
    let endFrame = frameInfo.endFrame;
    let length = Math.floor(endFrame - startFrame);
    let array = new Array(length);

    // v(t) = linearly interpolate between V[k] and V[k + 1] where k =
    // floor((N-1)/duration*(t - t0))
    let f = (length - 1) / (endTime - startTime);

    for (let k = 0; k < length; ++k) {
      let t = (startFrame + k) / sampleRate;
      let indexFloat = f * (t - startTime);
      let index = Math.floor(indexFloat);
      if (index + 1 < length) {
        let v0 = curve[index];
        let v1 = curve[index + 1];
        array[k] = v0 + (v1 - v0) * (indexFloat - index);
      } else {
        array[k] = curve[length - 1];
      }
    }

    return array;
  }

  // Create a sine wave of the given frequency and amplitude.  The sine wave is
  // offset by half the amplitude so that result is always positive.
  function createSineWaveArray(durationSeconds, freqHz, amplitude, sampleRate) {
    let length = timeToSampleFrame(durationSeconds, sampleRate);
    let signal = new Float32Array(length);
    let omega = 2 * Math.PI * freqHz / sampleRate;
    let halfAmplitude = amplitude / 2;

    for (let k = 0; k < length; ++k) {
      signal[k] = halfAmplitude + halfAmplitude * Math.sin(omega * k);
    }

    return signal;
  }

  // Return the difference between the starting value and the ending value for
  // time interval |timeIntervalIndex|.  We alternate between an end value that
  // is above or below the starting value.
  function endValueDelta(timeIntervalIndex) {
    if (timeIntervalIndex & 1) {
      return -startEndValueChange;
    } else {
      return startEndValueChange;
    }
  }

  // Relative error metric
  function relativeErrorMetric(actual, expected) {
    return (actual - expected) / Math.abs(expected);
  }

  // Difference metric
  function differenceErrorMetric(actual, expected) {
    return actual - expected;
  }

  // Return the difference between the starting value at |timeIntervalIndex| and
  // the starting value at the next time interval.  Since we started at a large
  // initial value, we decrease the value at each time interval.
  function valueUpdate(timeIntervalIndex) {
    return -startingValueDelta;
  }

  // Compare a section of the rendered data against our expected signal.
  function comparePartialSignals(
      should, rendered, expectedFunction, startTime, endTime, valueInfo,
      sampleRate, errorMetric) {
    let startSample = timeToSampleFrame(startTime, sampleRate);
    let expected = expectedFunction(
        startTime, endTime, valueInfo.startValue, valueInfo.endValue,
        sampleRate, timeConstant);

    let n = expected.length;
    let maxError = -1;
    let maxErrorIndex = -1;

    for (let k = 0; k < n; ++k) {
      // Make sure we don't pass these tests because a NaN has been generated in
      // either the
      // rendered data or the reference data.
      if (!isValidNumber(rendered[startSample + k])) {
        maxError = Infinity;
        maxErrorIndex = startSample + k;
        should(
            isValidNumber(rendered[startSample + k]),
            'NaN or infinity for rendered data at ' + maxErrorIndex)
            .beTrue();
        break;
      }
      if (!isValidNumber(expected[k])) {
        maxError = Infinity;
        maxErrorIndex = startSample + k;
        should(
            isValidNumber(expected[k]),
            'NaN or infinity for rendered data at ' + maxErrorIndex)
            .beTrue();
        break;
      }
      let error = Math.abs(errorMetric(rendered[startSample + k], expected[k]));
      if (error > maxError) {
        maxError = error;
        maxErrorIndex = k;
      }
    }

    return {maxError: maxError, index: maxErrorIndex, expected: expected};
  }

  // Find the discontinuities in the data and compare the locations of the
  // discontinuities with the times that define the time intervals. There is a
  // discontinuity if the difference between successive samples exceeds the
  // threshold.
  function verifyDiscontinuities(should, values, times, threshold) {
    let n = values.length;
    let success = true;
    let badLocations = 0;
    let breaks = [];

    // Find discontinuities.
    for (let k = 1; k < n; ++k) {
      if (Math.abs(values[k] - values[k - 1]) > threshold) {
        breaks.push(k);
      }
    }

    let testCount;

    // If there are numberOfTests intervals, there are only numberOfTests - 1
    // internal interval boundaries. Hence the maximum number of discontinuties
    // we expect to find is numberOfTests - 1. If we find more than that, we
    // have no reference to compare against. We also assume that the actual
    // discontinuities are close to the expected ones.
    //
    // This is just a sanity check when something goes really wrong.  For
    // example, if the threshold is too low, every sample frame looks like a
    // discontinuity.
    if (breaks.length >= numberOfTests) {
      testCount = numberOfTests - 1;
      should(breaks.length, 'Number of discontinuities')
          .beLessThan(numberOfTests);
      success = false;
    } else {
      testCount = breaks.length;
    }

    // Compare the location of each discontinuity with the end time of each
    // interval. (There is no discontinuity at the start of the signal.)
    for (let k = 0; k < testCount; ++k) {
      let expectedSampleFrame = timeToSampleFrame(times[k + 1], sampleRate);
      if (breaks[k] != expectedSampleFrame) {
        success = false;
        ++badLocations;
        should(breaks[k], 'Discontinuity at index')
            .beEqualTo(expectedSampleFrame);
      }
    }

    if (badLocations) {
      should(badLocations, 'Number of discontinuites at incorrect locations')
          .beEqualTo(0);
      success = false;
    } else {
      should(
          breaks.length + 1,
          'Number of tests started and ended at the correct time')
          .beEqualTo(numberOfTests);
    }

    return success;
  }

  // Compare the rendered data with the expected data.
  //
  // testName - string describing the test
  //
  // maxError - maximum allowed difference between the rendered data and the
  // expected data
  //
  // rendererdData - array containing the rendered (actual) data
  //
  // expectedFunction - function to compute the expected data
  //
  // timeValueInfo - array containing information about the start and end times
  // and the start and end values of each interval.
  //
  // breakThreshold - threshold to use for determining discontinuities.
  function compareSignals(
      should, testName, maxError, renderedData, expectedFunction, timeValueInfo,
      breakThreshold, errorMetric) {
    let success = true;
    let failedTestCount = 0;
    let times = timeValueInfo.times;
    let values = timeValueInfo.values;
    let n = values.length;
    let expectedSignal = [];

    success =
        verifyDiscontinuities(should, renderedData, times, breakThreshold);

    for (let k = 0; k < n; ++k) {
      let result = comparePartialSignals(
          should, renderedData, expectedFunction, times[k], times[k + 1],
          values[k], sampleRate, errorMetric);

      expectedSignal =
          expectedSignal.concat(Array.prototype.slice.call(result.expected));

      should(
          result.maxError,
          'Max error for test ' + k + ' at offset ' +
              (result.index + timeToSampleFrame(times[k], sampleRate)))
          .beLessThanOrEqualTo(maxError);
    }

    should(
        failedTestCount,
        'Number of failed tests with an acceptable relative tolerance of ' +
            maxError)
        .beEqualTo(0);
  }

  // Create a function to test the rendered data with the reference data.
  //
  // testName - string describing the test
  //
  // error - max allowed error between rendered data and the reference data.
  //
  // referenceFunction - function that generates the reference data to be
  // compared with the rendered data.
  //
  // jumpThreshold - optional parameter that specifies the threshold to use for
  // detecting discontinuities.  If not specified, defaults to
  // discontinuityThreshold.
  //
  function checkResultFunction(
      task, should, testName, error, referenceFunction, jumpThreshold,
      errorMetric) {
    return function(event) {
      let buffer = event.renderedBuffer;
      renderedData = buffer.getChannelData(0);

      let threshold;

      if (!jumpThreshold) {
        threshold = discontinuityThreshold;
      } else {
        threshold = jumpThreshold;
      }

      compareSignals(
          should, testName, error, renderedData, referenceFunction,
          timeValueInfo, threshold, errorMetric);
      task.done();
    }
  }

  // Run all the automation tests.
  //
  // numberOfTests - number of tests (time intervals) to run.
  //
  // initialValue - The initial value of the first time interval.
  //
  // setValueFunction - function that sets the specified value at the start of a
  // time interval.
  //
  // automationFunction - function that sets the end value for the time
  // interval. It specifies how the value approaches the end value.
  //
  // An object is returned containing an array of start times for each time
  // interval, and an array giving the start and end values for the interval.
  function doAutomation(
      numberOfTests, initialValue, setValueFunction, automationFunction) {
    let timeInfo = [0];
    let valueInfo = [];
    let value = initialValue;

    for (let k = 0; k < numberOfTests; ++k) {
      let startTime = k * timeInterval;
      let endTime = (k + 1) * timeInterval;
      let endValue = value + endValueDelta(k);

      // Set the value at the start of the time interval.
      setValueFunction(value, startTime);

      // Specify the end or target value, and how we should approach it.
      automationFunction(endValue, startTime, endTime);

      // Keep track of the start times, and the start and end values for each
      // time interval.
      timeInfo.push(endTime);
      valueInfo.push({startValue: value, endValue: endValue});

      value += valueUpdate(k);
    }

    return {times: timeInfo, values: valueInfo};
  }

  // Create the audio graph for the test and then run the test.
  //
  // numberOfTests - number of time intervals (tests) to run.
  //
  // initialValue - the initial value of the gain at time 0.
  //
  // setValueFunction - function to set the value at the beginning of each time
  // interval.
  //
  // automationFunction - the AudioParamTimeline automation function
  //
  // testName - string indicating the test that is being run.
  //
  // maxError - maximum allowed error between the rendered data and the
  // reference data
  //
  // referenceFunction - function that generates the reference data to be
  // compared against the rendered data.
  //
  // jumpThreshold - optional parameter that specifies the threshold to use for
  // detecting discontinuities.  If not specified, defaults to
  // discontinuityThreshold.
  //
  function createAudioGraphAndTest(
      task, should, numberOfTests, initialValue, setValueFunction,
      automationFunction, testName, maxError, referenceFunction, jumpThreshold,
      errorMetric) {
    // Create offline audio context.
    context =
        new OfflineAudioContext(2, renderLength(numberOfTests), sampleRate);
    let constantBuffer =
        createConstantBuffer(context, renderLength(numberOfTests), 1);

    // We use an AudioGainNode here simply as a convenient way to test the
    // AudioParam automation, since it's easy to pass a constant value through
    // the node, automate the .gain attribute and observe the resulting values.

    gainNode = context.createGain();

    let bufferSource = context.createBufferSource();
    bufferSource.buffer = constantBuffer;
    bufferSource.connect(gainNode);
    gainNode.connect(context.destination);

    // Set up default values for the parameters that control how the automation
    // test values progress for each time interval.
    startingValueDelta = initialValue / numberOfTests;
    startEndValueChange = startingValueDelta / 2;
    discontinuityThreshold = startEndValueChange / 2;

    // Run the automation tests.
    timeValueInfo = doAutomation(
        numberOfTests, initialValue, setValueFunction, automationFunction);
    bufferSource.start(0);

    context.oncomplete = checkResultFunction(
        task, should, testName, maxError, referenceFunction, jumpThreshold,
        errorMetric || relativeErrorMetric);
    context.startRendering();
  }

  // Export local references to global scope. All the new objects in this file
  // must be exported through this if it is to be used in the actual test HTML
  // page.
  let exports = {
    'sampleRate': 44100,
    'gainNode': null,
    'timeInterval': timeIntervalInternal,

    // Some suitable time constant so that we can see a significant change over
    // a timeInterval.  This is only needed by setTargetAtTime() which needs a
    // time constant.
    'timeConstant': timeIntervalInternal / 3,

    'renderLength': renderLength,
    'createConstantArray': createConstantArray,
    'getStartEndFrames': getStartEndFrames,
    'createLinearRampArray': createLinearRampArray,
    'createExponentialRampArray': createExponentialRampArray,
    'discreteTimeConstantForSampleRate': discreteTimeConstantForSampleRate,
    'createExponentialApproachArray': createExponentialApproachArray,
    'createReferenceSineArray': createReferenceSineArray,
    'createSineWaveArray': createSineWaveArray,
    'endValueDelta': endValueDelta,
    'relativeErrorMetric': relativeErrorMetric,
    'differenceErrorMetric': differenceErrorMetric,
    'valueUpdate': valueUpdate,
    'comparePartialSignals': comparePartialSignals,
    'verifyDiscontinuities': verifyDiscontinuities,
    'compareSignals': compareSignals,
    'checkResultFunction': checkResultFunction,
    'doAutomation': doAutomation,
    'createAudioGraphAndTest': createAudioGraphAndTest
  };

  for (let reference in exports) {
    global[reference] = exports[reference];
  }

})(window);