a59f67d854f2b02fd12d4dbc9dce235f2c157189
[WebKit-https.git] / LayoutTests / webaudio / resources / audioparam-testing.js
1 var sampleRate = 44100;
2
3 // Information about the starting/ending times and starting/ending values for each time interval.
4 var timeValueInfo;
5
6 // The difference between starting values between each time interval.
7 var startingValueDelta;
8       
9 // For any automation function that has an end or target value, the end value is based the starting
10 // value of the time interval.  The starting value will be increased or decreased by
11 // |startEndValueChange|. We choose half of |startingValueDelta| so that the ending value will be
12 // distinct from the starting value for next time interval.  This allows us to detect where the ramp
13 // begins and ends.
14 var startEndValueChange;
15
16 // Default threshold to use for detecting discontinuities that should appear at each time interval.
17 var discontinuityThreshold;
18
19 // Time interval between value changes.  It is best if 1 / numberOfTests is not close to timeInterval.
20 var timeInterval = .03;
21
22 // Some suitable time constant so that we can see a significant change over a timeInterval.  This is
23 // only needed by setTargetAtTime() which needs a time constant.
24 var timeConstant = timeInterval / 3;
25
26 var gainNode;
27
28 var context;
29
30 // Make sure we render long enough to capture all of our test data.
31 function renderLength(numberOfTests)
32 {
33     return timeToSampleFrame((numberOfTests + 1) * timeInterval, sampleRate);
34 }
35
36 // Create a buffer containing the same constant value.
37 function createConstantBuffer(context, constant, length) {
38     var buffer = context.createBuffer(1, length, context.sampleRate);
39     var n = buffer.length;
40     var data = buffer.getChannelData(0);
41
42     for (var k = 0; k < n; ++k) {
43         data[k] = constant;
44     }
45
46     return buffer;
47 }
48
49 // Create a constant reference signal with the given |value|.  Basically the same as
50 // |createConstantBuffer|, but with the parameters to match the other create functions.  The
51 // |endValue| is ignored.
52 function createConstantArray(startTime, endTime, value, endValue, sampleRate)
53 {
54     var startFrame = timeToSampleFrame(startTime, sampleRate);
55     var endFrame = timeToSampleFrame(endTime, sampleRate);
56     var length = endFrame - startFrame;
57
58     var buffer = createConstantBuffer(context, value, length);
59
60     return buffer.getChannelData(0);
61 }
62
63 // Create a linear ramp starting at |startValue| and ending at |endValue|.  The ramp starts at time
64 // |startTime| and ends at |endTime|.  (The start and end times are only used to compute how many
65 // samples to return.)
66 function createLinearRampArray(startTime, endTime, startValue, endValue, sampleRate)
67 {
68     var startFrame = timeToSampleFrame(startTime, sampleRate);
69     var endFrame = timeToSampleFrame(endTime, sampleRate);
70     var length = endFrame - startFrame;
71     var array = new Array(length);
72
73     var step = (endValue - startValue) / length;
74
75     for (k = 0; k < length; ++k) {
76         array[k] = startValue + k * step;
77     }
78
79     return array;
80 }
81
82 // Create an exponential ramp starting at |startValue| and ending at |endValue|.  The ramp starts at
83 // time |startTime| and ends at |endTime|.  (The start and end times are only used to compute how
84 // many samples to return.)
85 function createExponentialRampArray(startTime, endTime, startValue, endValue, sampleRate)
86 {
87     var startFrame = timeToSampleFrame(startTime, sampleRate);
88     var endFrame = timeToSampleFrame(endTime, sampleRate);
89     var length = endFrame - startFrame;
90     var array = new Array(length);
91
92     var multiplier = Math.pow(endValue / startValue, 1 / length);
93     
94     for (var k = 0; k < length; ++k) {
95         array[k] = startValue * Math.pow(multiplier, k);
96     }
97
98     return array;
99 }
100
101 function discreteTimeConstantForSampleRate(timeConstant, sampleRate)
102 {
103     return 1 - Math.exp(-1 / (sampleRate * timeConstant));
104 }
105
106 // Create a signal that starts at |startValue| and exponentially approaches the target value of
107 // |targetValue|, using a time constant of |timeConstant|.  The ramp starts at time |startTime| and
108 // ends at |endTime|.  (The start and end times are only used to compute how many samples to
109 // return.)
110 function createExponentialApproachArray(startTime, endTime, startValue, targetValue, sampleRate, timeConstant)
111 {
112     var startFrame = timeToSampleFrame(startTime, sampleRate);
113     var endFrame = timeToSampleFrame(endTime, sampleRate);
114     var length = endFrame - startFrame;
115     var array = new Array(length);
116     var c = discreteTimeConstantForSampleRate(timeConstant, sampleRate);
117
118     var value = startValue;
119     
120     for (var k = 0; k < length; ++k) {
121         array[k] = value;
122         value += (targetValue - value) * c;
123     }
124
125     return array;
126 }
127
128 // Create a sine wave of the given frequency and amplitude.  The sine wave is offset by half the
129 // amplitude so that result is always positive.
130 function createSineWaveArray(durationSeconds, freqHz, amplitude, sampleRate)
131 {
132     var length = timeToSampleFrame(durationSeconds, sampleRate);
133     var signal = new Float32Array(length);
134     var omega = 2 * Math.PI * freqHz / sampleRate;
135     var halfAmplitude = amplitude / 2;
136     
137     for (var k = 0; k < length; ++k) {
138         signal[k] = halfAmplitude + halfAmplitude * Math.sin(omega * k);
139     }
140
141     return signal;
142 }
143
144 // Return the difference between the starting value and the ending value for time interval
145 // |timeIntervalIndex|.  We alternate between an end value that is above or below the starting
146 // value.
147 function endValueDelta(timeIntervalIndex)
148 {
149     if (timeIntervalIndex & 1) {
150         return -startEndValueChange;
151     } else {
152         return startEndValueChange;
153     }
154 }
155
156 // Return the difference between the starting value at |timeIntervalIndex| and the starting value at
157 // the next time interval.  Since we started at a large initial value, we decrease the value at each
158 // time interval.
159 function valueUpdate(timeIntervalIndex)
160 {
161     return -startingValueDelta;
162 }
163
164 // Compare a section of the rendered data against our expected signal.
165 function comparePartialSignals(rendered, expectedFunction, startTime, endTime, valueInfo, sampleRate)
166 {
167     var startSample = timeToSampleFrame(startTime, sampleRate);
168     var expected = expectedFunction(startTime, endTime, valueInfo.startValue, valueInfo.endValue, sampleRate, timeConstant);
169
170     var n = expected.length;
171     var maxError = -1;
172     var maxErrorIndex = -1;
173     
174     for (var k = 0; k < n; ++k) {
175         // Make sure we don't pass these tests because a NaN has been generated in either the
176         // rendered data or the reference data.
177         if (!isValidNumber(rendered[startSample + k])) {
178             maxError = Infinity;
179             maxErrorIndex = startSample + k;
180             testFailed("NaN or infinity for rendered data at " + maxErrorIndex);
181             break;
182         }
183         if (!isValidNumber(expected[k])) {
184             maxError = Infinity;
185             maxErrorIndex = startSample + k;
186             testFailed("Nan or infinity for reference data at " + maxErrorIndex);
187             break;
188         }
189         var error = Math.abs(rendered[startSample + k] - expected[k]);
190         if (error > maxError) {
191             maxError = error;
192             maxErrorIndex = k;
193         }
194     }
195
196     return {maxError : maxError, index : maxErrorIndex};
197 }
198
199 // Find the discontinuities in the data and compare the locations of the discontinuities with the
200 // times that define the time intervals. There is a discontinuity if the difference between
201 // successive samples exceeds the threshold.
202 function verifyDiscontinuities(values, times, threshold)
203 {
204     var n = values.length;
205     var success = true;
206     var badLocations = 0;
207     var breaks = [];
208
209     // Find discontinuities.
210     for (var k = 1; k < n; ++k) {
211         if (Math.abs(values[k] - values[k - 1]) > threshold) {
212             breaks.push(k);
213         }
214     }
215
216     var testCount;
217
218     // If there are numberOfTests intervals, there are only numberOfTests - 1 internal interval
219     // boundaries. Hence the maximum number of discontinuties we expect to find is numberOfTests -
220     // 1. If we find more than that, we have no reference to compare against. We also assume that
221     // the actual discontinuities are close to the expected ones.
222     //
223     // This is just a sanity check when something goes really wrong.  For example, if the threshold
224     // is too low, every sample frame looks like a discontinuity.
225     if (breaks.length >= numberOfTests) {
226         testCount = numberOfTests - 1;
227         testFailed("Found more discontinuities (" + breaks.length + ") than expected.  Only comparing first " + testCount + "discontinuities.");
228         success = false;
229     } else {
230         testCount = breaks.length;
231     }
232     
233     // Compare the location of each discontinuity with the end time of each interval. (There is no
234     // discontinuity at the start of the signal.)
235     for (var k = 0; k < testCount; ++k) {
236         var expectedSampleFrame = timeToSampleFrame(times[k + 1], sampleRate);
237         if (breaks[k] != expectedSampleFrame) {
238             success = false;
239             ++badLocations;
240             testFailed("Expected discontinuity at " + expectedSampleFrame + " but got " + breaks[k]);
241         }
242     }
243
244     if (badLocations) {
245         testFailed(badLocations + " discontinuities at incorrect locations");
246         success = false;
247     } else {
248         if (breaks.length == numberOfTests - 1) {
249             testPassed("All " + numberOfTests + " tests started and ended at the correct time.");
250         } else {
251             testFailed("Found " + breaks.length + " discontinuities but expected " + (numberOfTests - 1));
252             success = false;
253         }
254     }
255     
256     return success;
257 }
258
259 // Compare the rendered data with the expected data.
260 //
261 // testName - string describing the test
262 //
263 // maxError - maximum allowed difference between the rendered data and the expected data
264 //
265 // rendererdData - array containing the rendered (actual) data
266 //
267 // expectedFunction - function to compute the expected data
268 //
269 // timeValueInfo - array containing information about the start and end times and the start and end
270 // values of each interval.
271 //
272 // breakThreshold - threshold to use for determining discontinuities.
273 function compareSignals(testName, maxError, renderedData, expectedFunction, timeValueInfo, breakThreshold)
274 {
275     var success = true;
276     var failedTestCount = 0;
277     var times = timeValueInfo.times;
278     var values = timeValueInfo.values;
279     var n = values.length;
280
281     success = verifyDiscontinuities(renderedData, times, breakThreshold);
282
283     for (var k = 0; k < n; ++k) {
284         var result = comparePartialSignals(renderedData, expectedFunction, times[k], times[k + 1], values[k], sampleRate);
285
286         if (result.maxError > maxError) {
287             testFailed("Incorrect value for test " + k + ". Max error = " + result.maxError + " at offset " + (result.index + timeToSampleFrame(times[k], sampleRate)));
288             ++failedTestCount;
289         }
290     }
291
292     if (failedTestCount) {
293         testFailed(failedTestCount + " tests failed out of " + n);
294         success = false;
295     } else {
296         testPassed("All " + n + " tests passed within an acceptable tolerance.");
297     }
298       
299     if (success) {
300         testPassed("AudioParam " + testName + " test passed.");
301     } else {
302         testFailed("AudioParam " + testName + " test failed.");
303     }
304 }
305
306 // Create a function to test the rendered data with the reference data.
307 //
308 // testName - string describing the test
309 //
310 // error - max allowed error between rendered data and the reference data.
311 //
312 // referenceFunction - function that generates the reference data to be compared with the rendered
313 // data.
314 //
315 // jumpThreshold - optional parameter that specifies the threshold to use for detecting
316 // discontinuities.  If not specified, defaults to discontinuityThreshold.
317 //
318 function checkResultFunction(testName, error, referenceFunction, jumpThreshold)
319 {
320     return function(event) {
321         var buffer = event.renderedBuffer;
322         renderedData = buffer.getChannelData(0);
323
324         var threshold;
325
326         if (!jumpThreshold) {
327             threshold = discontinuityThreshold;
328         } else {
329             threshold = jumpThreshold;
330         }
331         
332         compareSignals(testName, error, renderedData, referenceFunction, timeValueInfo, threshold);
333
334         finishJSTest();
335     }
336 }
337
338 // Run all the automation tests.
339 //
340 // numberOfTests - number of tests (time intervals) to run.
341 //
342 // initialValue - The initial value of the first time interval.
343 //
344 // setValueFunction - function that sets the specified value at the start of a time interval.
345 //
346 // automationFunction - function that sets the end value for the time interval.  It specifies how
347 // the value approaches the end value.
348 //
349 // An object is returned containing an array of start times for each time interval, and an array
350 // giving the start and end values for the interval.
351 function doAutomation(numberOfTests, initialValue, setValueFunction, automationFunction)
352 {
353     var timeInfo = [0];
354     var valueInfo = [];
355     var value = initialValue;
356     
357     for (var k = 0; k < numberOfTests; ++k) {
358         var startTime = k * timeInterval;
359         var endTime = (k + 1) * timeInterval;
360         var endValue = value + endValueDelta(k);
361
362         // Set the value at the start of the time interval.
363         setValueFunction(value, startTime);
364
365         // Specify the end or target value, and how we should approach it.
366         automationFunction(endValue, startTime, endTime);
367
368         // Keep track of the start times, and the start and end values for each time interval.
369         timeInfo.push(endTime);
370         valueInfo.push({startValue: value, endValue : endValue});
371
372         value += valueUpdate(k);
373     }
374
375     return {times : timeInfo, values : valueInfo};
376 }
377
378 // Create the audio graph for the test and then run the test.
379 //
380 // numberOfTests - number of time intervals (tests) to run.
381 //
382 // initialValue - the initial value of the gain at time 0.
383 //
384 // setValueFunction - function to set the value at the beginning of each time interval.
385 //
386 // automationFunction - the AudioParamTimeline automation function
387 //
388 // testName - string indicating the test that is being run.
389 //
390 // maxError - maximum allowed error between the rendered data and the reference data
391 //
392 // referenceFunction - function that generates the reference data to be compared against the
393 // rendered data.
394 //
395 // jumpThreshold - optional parameter that specifies the threshold to use for detecting
396 // discontinuities.  If not specified, defaults to discontinuityThreshold.
397 //
398 function createAudioGraphAndTest(numberOfTests, initialValue, setValueFunction, automationFunction, testName, maxError, referenceFunction, jumpThreshold)
399 {
400     if (window.testRunner) {
401         testRunner.dumpAsText();
402         testRunner.waitUntilDone();
403     }
404
405     window.jsTestIsAsync = true;
406
407     // Create offline audio context.
408     context = new webkitAudioContext(2, renderLength(numberOfTests), sampleRate);
409     var constantBuffer = createConstantBuffer(context, 1, renderLength(numberOfTests));
410
411     // We use an AudioGainNode here simply as a convenient way to test the AudioParam
412     // automation, since it's easy to pass a constant value through the node, automate the
413     // .gain attribute and observe the resulting values.
414
415     gainNode = context.createGainNode();
416
417     var bufferSource = context.createBufferSource();
418     bufferSource.buffer = constantBuffer;
419     bufferSource.connect(gainNode);
420     gainNode.connect(context.destination);
421
422     // Set up default values for the parameters that control how the automation test values progress
423     // for each time interval.
424     startingValueDelta = initialValue / numberOfTests;
425     startEndValueChange = startingValueDelta / 2;
426     discontinuityThreshold = startEndValueChange / 2;
427
428     // Run the automation tests.
429     timeValueInfo = doAutomation(numberOfTests,
430                                  initialValue,
431                                  setValueFunction,
432                                  automationFunction);
433     bufferSource.noteOn(0);
434       
435     context.oncomplete = checkResultFunction(testName,
436                                              maxError,
437                                              referenceFunction,
438                                              jumpThreshold);
439     context.startRendering();
440 }