commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 1 | <!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.com | c8e7c72 | 2017-05-22 20:49:12 +0000 | [diff] [blame] | 6 | <script src="../resources/js-test.js"></script> |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 7 | </head> |
| 8 | |
| 9 | <body> |
| 10 | <div id="description"></div> |
| 11 | <div id="console"></div> |
| 12 | |
| 13 | <script> |
| 14 | description("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 | |
| 21 | var context; |
| 22 | |
| 23 | // The biquad filter node. |
| 24 | var filter; |
| 25 | |
| 26 | // The magnitude response of the biquad filter. |
| 27 | var magResponse; |
| 28 | |
| 29 | // The phase response of the biquad filter. |
| 30 | var phaseResponse; |
| 31 | |
| 32 | // Number of frequency samples to take. |
| 33 | var numberOfFrequencies = 1000; |
| 34 | |
| 35 | // The filter parameters. |
| 36 | var filterCutoff = 1000; // Hz. |
| 37 | var filterQ = 1; |
| 38 | var filterGain = 5; // Decibels. |
| 39 | |
| 40 | // The maximum allowed error in the magnitude response. |
| 41 | var maxAllowedMagError = 5.7e-7; |
| 42 | |
| 43 | // The maximum allowed error in the phase response. |
| 44 | var maxAllowedPhaseError = 4.7e-8; |
| 45 | |
| 46 | // The magnitudes and phases of the reference frequency response. |
| 47 | var magResponse; |
| 48 | var phaseResponse; |
| 49 | |
| 50 | // The magnitudes and phases of the reference frequency response. |
| 51 | var expectedMagnitudes; |
| 52 | var expectedPhases; |
| 53 | |
| 54 | // Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the |
| 55 | // Nyquist frequency. |
| 56 | function 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|. |
| 63 | function 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|. |
| 106 | function frequencyResponseReference(filter, frequencies) |
| 107 | { |
| 108 | var sampleRate = filter.context.sampleRate; |
| 109 | var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate); |
weinig@apple.com | d61d8c1 | 2016-08-27 02:01:11 +0000 | [diff] [blame] | 110 | var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value); |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 111 | |
| 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. |
| 125 | function 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 | |
| 138 | function 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. |
| 149 | function 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. |
| 161 | function 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. |
| 172 | function 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 | |
| 272 | function runTest() |
| 273 | { |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 274 | 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.com | 0430445 | 2013-01-04 21:33:16 +0000 | [diff] [blame] | 281 | filter.type = "peaking"; |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 282 | 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.com | 0430445 | 2013-01-04 21:33:16 +0000 | [diff] [blame] | 294 | testPassed("Frequency response was correct."); |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 295 | } else { |
crogers@google.com | 0430445 | 2013-01-04 21:33:16 +0000 | [diff] [blame] | 296 | testFailed("Frequency response was incorrect."); |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 297 | } |
| 298 | |
| 299 | finishJSTest(); |
| 300 | } |
| 301 | |
| 302 | runTest(); |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 303 | |
| 304 | </script> |
commit-queue@webkit.org | 589a0c3 | 2012-03-03 18:55:49 +0000 | [diff] [blame] | 305 | </body> |
| 306 | </html> |