summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-audioparam-interface/event-insertion.html
blob: b846f982ab29faf2010dfac249f7ba031dcba172 (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
<!doctype html>
<html>
  <head>
    <title>
      Test Handling of Event Insertion
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
    <script src="/webaudio/resources/audio-param.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let audit = Audit.createTaskRunner();

      // Use a power of two for the sample rate so there's no round-off in
      // computing time from frame.
      let sampleRate = 16384;

      audit.define(
          {label: 'Insert same event at same time'}, (task, should) => {
            // Context for testing.
            let context = new OfflineAudioContext(
                {length: 16384, sampleRate: sampleRate});

            // The source node to use.  Automations will be scheduled here.
            let src = new ConstantSourceNode(context, {offset: 0});
            src.connect(context.destination);

            // An array of tests to be done.  Each entry specifies the event
            // type and the event time.  The events are inserted in the order
            // given (in |values|), and the second event should be inserted
            // after the first one, as required by the spec.
            let testCases = [
              {
                event: 'setValueAtTime',
                frame: RENDER_QUANTUM_FRAMES,
                values: [99, 1],
                outputTestFrame: RENDER_QUANTUM_FRAMES,
                expectedOutputValue: 1
              },
              {
                event: 'linearRampToValueAtTime',
                frame: 2 * RENDER_QUANTUM_FRAMES,
                values: [99, 2],
                outputTestFrame: 2 * RENDER_QUANTUM_FRAMES,
                expectedOutputValue: 2
              },
              {
                event: 'exponentialRampToValueAtTime',
                frame: 3 * RENDER_QUANTUM_FRAMES,
                values: [99, 3],
                outputTestFrame: 3 * RENDER_QUANTUM_FRAMES,
                expectedOutputValue: 3
              },
              {
                event: 'setValueCurveAtTime',
                frame: 3 * RENDER_QUANTUM_FRAMES,
                values: [[3, 4]],
                extraArgs: RENDER_QUANTUM_FRAMES / context.sampleRate,
                outputTestFrame: 4 * RENDER_QUANTUM_FRAMES,
                expectedOutputValue: 4
              },
              {
                event: 'setValueAtTime',
                frame: 5 * RENDER_QUANTUM_FRAMES - 1,
                values: [99, 1, 5],
                outputTestFrame: 5 * RENDER_QUANTUM_FRAMES,
                expectedOutputValue: 5
              }
            ];

            testCases.forEach(entry => {
              entry.values.forEach(value => {
                let eventTime = entry.frame / context.sampleRate;
                let message = eventToString(
                    entry.event, value, eventTime, entry.extraArgs);
                // This is mostly to print out the event that is getting
                // inserted.  It should never ever throw.
                should(() => {
                  src.offset[entry.event](value, eventTime, entry.extraArgs);
                }, message).notThrow();
              });
            });

            src.start();

            context.startRendering()
                .then(audioBuffer => {
                  let audio = audioBuffer.getChannelData(0);

                  // Look through the test cases to figure out what the correct
                  // output values should be.
                  testCases.forEach(entry => {
                    let expected = entry.expectedOutputValue;
                    let frame = entry.outputTestFrame;
                    let time = frame / context.sampleRate;
                    should(
                        audio[frame], `Output at frame ${frame} (time ${time})`)
                        .beEqualTo(expected);
                  });
                })
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'Linear + Expo',
            description: 'Different events at same time'
          },
          (task, should) => {
            // Should be a linear ramp up to the event time, and after a
            // constant value because the exponential ramp has ended.
            let testCase = [
              {event: 'linearRampToValueAtTime', value: 2, relError: 0},
              {event: 'setValueAtTime', value: 99},
              {event: 'exponentialRampToValueAtTime', value: 3},
            ];
            let eventFrame = 2 * RENDER_QUANTUM_FRAMES;
            let prefix = 'Linear+Expo: ';

            testEventInsertion(prefix, should, eventFrame, testCase)
                .then(expectConstant(prefix, should, eventFrame, testCase))
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'Expo + Linear',
            description: 'Different events at same time',
          },
          (task, should) => {
            // Should be an exponential ramp up to the event time, and after a
            // constant value because the linear ramp has ended.
            let testCase = [
              {
                event: 'exponentialRampToValueAtTime',
                value: 3,
                relError: 4.2533e-6
              },
              {event: 'setValueAtTime', value: 99},
              {event: 'linearRampToValueAtTime', value: 2},
            ];
            let eventFrame = 2 * RENDER_QUANTUM_FRAMES;
            let prefix = 'Expo+Linear: ';

            testEventInsertion(prefix, should, eventFrame, testCase)
                .then(expectConstant(prefix, should, eventFrame, testCase))
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'Linear + SetTarget',
            description: 'Different events at same time',
          },
          (task, should) => {
            // Should be a linear ramp up to the event time, and then a
            // decaying value.
            let testCase = [
              {event: 'linearRampToValueAtTime', value: 3, relError: 0},
              {event: 'setValueAtTime', value: 100},
              {event: 'setTargetAtTime', value: 0, extraArgs: 0.1},
            ];
            let eventFrame = 2 * RENDER_QUANTUM_FRAMES;
            let prefix = 'Linear+SetTarget: ';

            testEventInsertion(prefix, should, eventFrame, testCase)
                .then(audioBuffer => {
                  let audio = audioBuffer.getChannelData(0);
                  let prefix = 'Linear+SetTarget: ';
                  let eventTime = eventFrame / sampleRate;
                  let expectedValue = methodMap[testCase[0].event](
                      (eventFrame - 1) / sampleRate, 1, 0, testCase[0].value,
                      eventTime);
                  should(
                      audio[eventFrame - 1],
                      prefix +
                          `At time ${
                                     (eventFrame - 1) / sampleRate
                                   } (frame ${eventFrame - 1}) output`)
                      .beCloseTo(
                          expectedValue,
                          {threshold: testCase[0].relError || 0});

                  // The setValue should have taken effect
                  should(
                      audio[eventFrame],
                      prefix +
                          `At time ${eventTime} (frame ${eventFrame}) output`)
                      .beEqualTo(testCase[1].value);

                  // The final event is setTarget.  Compute the expected output.
                  let actual = audio.slice(eventFrame);
                  let expected = new Float32Array(actual.length);
                  for (let k = 0; k < expected.length; ++k) {
                    let t = (eventFrame + k) / sampleRate;
                    expected[k] = audioParamSetTarget(
                        t, testCase[1].value, eventTime, testCase[2].value,
                        testCase[2].extraArgs);
                  }
                  should(
                      actual,
                      prefix +
                          `At time ${eventTime} (frame ${
                                                         eventFrame
                                                       }) and later`)
                      .beCloseToArray(expected, {relativeThreshold: 2.6694e-7});
                })
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'Multiple linear ramps at the same time',
            description: 'Verify output'
          },
          (task, should) => {
            testMultipleSameEvents(should, {
              method: 'linearRampToValueAtTime',
              prefix: 'Multiple linear ramps: ',
              threshold: 0
            }).then(() => task.done());
          });

      audit.define(
          {
            label: 'Multiple exponential ramps at the same time',
            description: 'Verify output'
          },
          (task, should) => {
            testMultipleSameEvents(should, {
              method: 'exponentialRampToValueAtTime',
              prefix: 'Multiple exponential ramps: ',
              threshold: 5.3924e-7
            }).then(() => task.done());
          });

      audit.run();

      // Takes a list of |testCases| consisting of automation methods and
      // schedules them to occur at |eventFrame|. |prefix| is a prefix for
      // messages produced by |should|.
      //
      // Each item in |testCases| is a dictionary with members:
      //   event     - the name of automation method to be inserted,
      //   value     - the value for the event,
      //   extraArgs - extra arguments if the event needs more than the value
      //               and time (such as setTargetAtTime).
      function testEventInsertion(prefix, should, eventFrame, testCases) {
        let context = new OfflineAudioContext(
            {length: 4 * RENDER_QUANTUM_FRAMES, sampleRate: sampleRate});

        // The source node to use.  Automations will be scheduled here.
        let src = new ConstantSourceNode(context, {offset: 0});
        src.connect(context.destination);

        // Initialize value to 1 at the beginning.
        src.offset.setValueAtTime(1, 0);

        // Test automations have this event time.
        let eventTime = eventFrame / context.sampleRate;

        // Sanity check that context is long enough for the test
        should(
            eventFrame < context.length,
            prefix + 'Context length is long enough for the test')
            .beTrue();

        // Automations to be tested.  The first event should be the actual
        // output up to the event time.  The last event should be the final
        // output from the event time and onwards.
        testCases.forEach(entry => {
          should(
              () => {
                src.offset[entry.event](
                    entry.value, eventTime, entry.extraArgs);
              },
              prefix +
                  eventToString(
                      entry.event, entry.value, eventTime, entry.extraArgs))
              .notThrow();
        });

        src.start();

        return context.startRendering();
      }

      // Verify output of test where the final value of the automation is
      // expected to be constant.
      function expectConstant(prefix, should, eventFrame, testCases) {
        return audioBuffer => {
          let audio = audioBuffer.getChannelData(0);

          let eventTime = eventFrame / sampleRate;

          // Compute the expected value of the first automation one frame before
          // the event time.  This is a quick check that the correct automation
          // was done.
          let expectedValue = methodMap[testCases[0].event](
              (eventFrame - 1) / sampleRate, 1, 0, testCases[0].value,
              eventTime);
          should(
              audio[eventFrame - 1],
              prefix +
                  `At time ${
                             (eventFrame - 1) / sampleRate
                           } (frame ${eventFrame - 1}) output`)
              .beCloseTo(expectedValue, {threshold: testCases[0].relError});

          // The last event scheduled is expected to set the value for all
          // future times.  Verify that the output has the expected value.
          should(
              audio.slice(eventFrame),
              prefix +
                  `At time ${eventTime} (frame ${
                                                 eventFrame
                                               }) and later, output`)
              .beConstantValueOf(testCases[testCases.length - 1].value);
        };
      }

      // Test output when two events of the same time are scheduled at the same
      // time.
      function testMultipleSameEvents(should, options) {
        let {method, prefix, threshold} = options;

        // Context for testing.
        let context =
            new OfflineAudioContext({length: 16384, sampleRate: sampleRate});

        let src = new ConstantSourceNode(context);
        src.connect(context.destination);

        let initialValue = 1;

        // Informative print
        should(() => {
          src.offset.setValueAtTime(initialValue, 0);
        }, prefix + `setValueAtTime(${initialValue}, 0)`).notThrow();

        let frame = 64;
        let time = frame / context.sampleRate;
        let values = [2, 7, 10];

        // Schedule two events of the same type at the same time, but with
        // different values.

        values.forEach(value => {
          // Informative prints to show what we're doing in this test.
          should(
              () => {
                src.offset[method](value, time);
              },
              prefix +
                  eventToString(
                      method,
                      value,
                      time,
                      ))
              .notThrow();
        })

        src.start();

        return context.startRendering().then(audioBuffer => {
          let actual = audioBuffer.getChannelData(0);

          // The output should be a ramp from time 0 to the event time.  But we
          // only verify the value just before the event time, which should be
          // fairly close to values[0].  (But compute the actual expected value
          // to be sure.)
          let expected = methodMap[method](
              (frame - 1) / context.sampleRate, initialValue, 0, values[0],
              time);
          should(actual[frame - 1], prefix + `Output at frame ${frame - 1}`)
              .beCloseTo(expected, {threshold: threshold, precision: 3});

          // Any other values shouldn't show up in the output.  Only the value
          // from last event should appear.  We only check the value at the
          // event time.
          should(
              actual[frame], prefix + `Output at frame ${frame} (${time} sec)`)
              .beEqualTo(values[values.length - 1]);
        });
      }

      // Convert an automation method to a string for printing.
      function eventToString(method, value, time, extras) {
        let string = method + '(';
        string += (value instanceof Array) ? `[${value}]` : value;
        string += ', ' + time;
        if (extras) {
          string += ', ' + extras;
        }
        string += ')';
        return string;
      }

      // Map between the automation method name and a function that computes the
      // output value of the automation method.
      const methodMap = {
        linearRampToValueAtTime: audioParamLinearRamp,
        exponentialRampToValueAtTime: audioParamExponentialRamp,
        setValueAtTime: (t, v) => v
      };
    </script>
  </body>
</html>