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