blob: 1028f64ddbdf3e9ae777d51b1e7bf981ad2bcd0a [file] [log] [blame]
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +00001<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
2<html>
3<head>
4<script src="resources/audio-testing.js"></script>
5<script src="resources/biquad-testing.js"></script>
ap@apple.comc8e7c722017-05-22 20:49:12 +00006<script src="../resources/js-test.js"></script>
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +00007</head>
8
9<body>
10<div id="description"></div>
11<div id="console"></div>
12
13<script>
14description("Test Biquad getFrequencyResponse() functionality.");
15
16// Test the frequency response of a biquad filter. We compute the frequency response for a simple
17// peaking biquad filter and compare it with the expected frequency response. The actual filter
18// used doesn't matter since we're testing getFrequencyResponse and not the actual filter output.
19// The filters are extensively tested in other biquad tests.
20
21var context;
22
23// The biquad filter node.
24var filter;
25
26// The magnitude response of the biquad filter.
27var magResponse;
28
29// The phase response of the biquad filter.
30var phaseResponse;
31
32// Number of frequency samples to take.
33var numberOfFrequencies = 1000;
34
35// The filter parameters.
36var filterCutoff = 1000; // Hz.
37var filterQ = 1;
38var filterGain = 5; // Decibels.
39
40// The maximum allowed error in the magnitude response.
41var maxAllowedMagError = 5.7e-7;
42
43// The maximum allowed error in the phase response.
44var maxAllowedPhaseError = 4.7e-8;
45
46// The magnitudes and phases of the reference frequency response.
47var magResponse;
48var phaseResponse;
49
50// The magnitudes and phases of the reference frequency response.
51var expectedMagnitudes;
52var expectedPhases;
53
54// Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the
55// Nyquist frequency.
56function normalizedFrequency(freqHz, sampleRate)
57{
58 var nyquist = sampleRate / 2;
59 return freqHz / nyquist;
60}
61
62// Get the filter response at a (normalized) frequency |f| for the filter with coefficients |coef|.
63function getResponseAt(coef, f)
64{
65 var b0 = coef.b0;
66 var b1 = coef.b1;
67 var b2 = coef.b2;
68 var a1 = coef.a1;
69 var a2 = coef.a2;
70
71 // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
72 //
73 // Compute H(exp(i * pi * f)). No native complex numbers in javascript, so break H(exp(i * pi * // f))
74 // in to the real and imaginary parts of the numerator and denominator. Let omega = pi * f.
75 // Then the numerator is
76 //
77 // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) + b2 * sin(2 * omega))
78 //
79 // and the denominator is
80 //
81 // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2 * sin(2 * omega))
82 //
83 // Compute the magnitude and phase from the real and imaginary parts.
84
85 var omega = Math.PI * f;
86 var numeratorReal = b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
87 var numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
88 var denominatorReal = 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
89 var denominatorImag = -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));
90
91 var magnitude = Math.sqrt((numeratorReal * numeratorReal + numeratorImag * numeratorImag)
92 / (denominatorReal * denominatorReal + denominatorImag * denominatorImag));
93 var phase = Math.atan2(numeratorImag, numeratorReal) - Math.atan2(denominatorImag, denominatorReal);
94
95 if (phase >= Math.PI) {
96 phase -= 2 * Math.PI;
97 } else if (phase <= -Math.PI) {
98 phase += 2 * Math.PI;
99 }
100
101 return {magnitude : magnitude, phase : phase};
102}
103
104// Compute the reference frequency response for the biquad filter |filter| at the frequency samples
105// given by |frequencies|.
106function frequencyResponseReference(filter, frequencies)
107{
108 var sampleRate = filter.context.sampleRate;
109 var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate);
weinig@apple.comd61d8c12016-08-27 02:01:11 +0000110 var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value);
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000111
112 var magnitudes = [];
113 var phases = [];
114
115 for (var k = 0; k < frequencies.length; ++k) {
116 var response = getResponseAt(filterCoefficients, normalizedFrequency(frequencies[k], sampleRate));
117 magnitudes.push(response.magnitude);
118 phases.push(response.phase);
119 }
120
121 return {magnitudes : magnitudes, phases : phases};
122}
123
124// Compute a set of linearly spaced frequencies.
125function createFrequencies(nFrequencies, sampleRate)
126{
127 var frequencies = new Float32Array(nFrequencies);
128 var nyquist = sampleRate / 2;
129 var freqDelta = nyquist / nFrequencies;
130
131 for (var k = 0; k < nFrequencies; ++k) {
132 frequencies[k] = k * freqDelta;
133 }
134
135 return frequencies;
136}
137
138function linearToDecibels(x)
139{
140 if (x) {
141 return 20 * Math.log(x) / Math.LN10;
142 } else {
143 return -1000;
144 }
145}
146
147// Look through the array and find any NaN or infinity. Returns the index of the first occurence or
148// -1 if none.
149function findBadNumber(signal)
150{
151 for (var k = 0; k < signal.length; ++k) {
152 if (!isValidNumber(signal[k])) {
153 return k;
154 }
155 }
156 return -1;
157}
158
159// Compute absolute value of the difference between phase angles, taking into account the wrapping
160// of phases.
161function absolutePhaseDifference(x, y)
162{
163 var diff = Math.abs(x - y);
164
165 if (diff > Math.PI) {
166 diff = 2 * Math.PI - diff;
167 }
168 return diff;
169}
170
171// Compare the frequency response with our expected response.
172function compareResponses(filter, frequencies, magResponse, phaseResponse)
173{
174 var expectedResponse = frequencyResponseReference(filter, frequencies);
175
176 expectedMagnitudes = expectedResponse.magnitudes;
177 expectedPhases = expectedResponse.phases;
178
179 var n = magResponse.length;
180 var success = true;
181 var badResponse = false;
182
183 var maxMagError = -1;
184 var maxMagErrorIndex = -1;
185
186 var k;
187 var hasBadNumber;
188
189 hasBadNumber = findBadNumber(magResponse);
190 if (hasBadNumber >= 0) {
191 testFailed("Magnitude response has NaN or infinity at " + hasBadNumber);
192 success = false;
193 badResponse = true;
194 }
195
196 hasBadNumber = findBadNumber(phaseResponse);
197 if (hasBadNumber >= 0) {
198 testFailed("Phase response has NaN or infinity at " + hasBadNumber);
199 success = false;
200 badResponse = true;
201 }
202
203 // These aren't testing the implementation itself. Instead, these are sanity checks on the
204 // reference. Failure here does not imply an error in the implementation.
205 hasBadNumber = findBadNumber(expectedMagnitudes);
206 if (hasBadNumber >= 0) {
207 testFailed("Expected magnitude response has NaN or infinity at " + hasBadNumber);
208 success = false;
209 badResponse = true;
210 }
211
212 hasBadNumber = findBadNumber(expectedPhases);
213 if (hasBadNumber >= 0) {
214 testFailed("Expected phase response has NaN or infinity at " + hasBadNumber);
215 success = false;
216 badResponse = true;
217 }
218
219 // If we found a NaN or infinity, the following tests aren't very helpful, especially for NaN.
220 // We run them anyway, after printing a warning message.
221
222 if (badResponse) {
223 testFailed("NaN or infinity in the actual or expected results makes the following test results suspect.");
224 success = false;
225 }
226
227 for (k = 0; k < n; ++k) {
228 var error = Math.abs(linearToDecibels(magResponse[k]) - linearToDecibels(expectedMagnitudes[k]));
229 if (error > maxMagError) {
230 maxMagError = error;
231 maxMagErrorIndex = k;
232 }
233 }
234
235 if (maxMagError > maxAllowedMagError) {
236 var message = "Magnitude error (" + maxMagError + " dB)";
237 message += " exceeded threshold at " + frequencies[maxMagErrorIndex];
238 message += " Hz. Actual: " + linearToDecibels(magResponse[maxMagErrorIndex]);
239 message += " dB, expected: " + linearToDecibels(expectedMagnitudes[maxMagErrorIndex]) + " dB.";
240 testFailed(message);
241 success = false;
242 } else {
243 testPassed("Magnitude response within acceptable threshold.");
244 }
245
246 var maxPhaseError = -1;
247 var maxPhaseErrorIndex = -1;
248
249 for (k = 0; k < n; ++k) {
250 var error = absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
251 if (error > maxPhaseError) {
252 maxPhaseError = error;
253 maxPhaseErrorIndex = k;
254 }
255 }
256
257 if (maxPhaseError > maxAllowedPhaseError) {
258 var message = "Phase error (radians) (" + maxPhaseError;
259 message += ") exceeded threshold at " + frequencies[maxPhaseErrorIndex];
260 message += " Hz. Actual: " + phaseResponse[maxPhaseErrorIndex];
261 message += " expected: " + expectedPhases[maxPhaseErrorIndex];
262 testFailed(message);
263 success = false;
264 } else {
265 testPassed("Phase response within acceptable threshold.");
266 }
267
268
269 return success;
270}
271
272function runTest()
273{
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000274 window.jsTestIsAsync = true;
275
276 context = new webkitAudioContext();
277
278 filter = context.createBiquadFilter();
279
280 // Arbitrarily test a peaking filter, but any kind of filter can be tested.
crogers@google.com04304452013-01-04 21:33:16 +0000281 filter.type = "peaking";
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000282 filter.frequency.value = filterCutoff;
283 filter.Q.value = filterQ;
284 filter.gain.value = filterGain;
285
286 var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
287 magResponse = new Float32Array(numberOfFrequencies);
288 phaseResponse = new Float32Array(numberOfFrequencies);
289
290 filter.getFrequencyResponse(frequencies, magResponse, phaseResponse);
291 var success = compareResponses(filter, frequencies, magResponse, phaseResponse);
292
293 if (success) {
crogers@google.com04304452013-01-04 21:33:16 +0000294 testPassed("Frequency response was correct.");
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000295 } else {
crogers@google.com04304452013-01-04 21:33:16 +0000296 testFailed("Frequency response was incorrect.");
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000297 }
298
299 finishJSTest();
300}
301
302runTest();
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000303
304</script>
commit-queue@webkit.org589a0c32012-03-03 18:55:49 +0000305</body>
306</html>