| 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 |