| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
| <html> |
| <head> |
| <script src="resources/audio-testing.js"></script> |
| <script src="resources/biquad-testing.js"></script> |
| <script src="../resources/js-test.js"></script> |
| </head> |
| |
| <body> |
| <div id="description"></div> |
| <div id="console"></div> |
| |
| <script> |
| description("Test Biquad getFrequencyResponse() functionality."); |
| |
| // Test the frequency response of a biquad filter. We compute the frequency response for a simple |
| // peaking biquad filter and compare it with the expected frequency response. The actual filter |
| // used doesn't matter since we're testing getFrequencyResponse and not the actual filter output. |
| // The filters are extensively tested in other biquad tests. |
| |
| var context; |
| |
| // The biquad filter node. |
| var filter; |
| |
| // The magnitude response of the biquad filter. |
| var magResponse; |
| |
| // The phase response of the biquad filter. |
| var phaseResponse; |
| |
| // Number of frequency samples to take. |
| var numberOfFrequencies = 1000; |
| |
| // The filter parameters. |
| var filterCutoff = 1000; // Hz. |
| var filterQ = 1; |
| var filterGain = 5; // Decibels. |
| |
| // The maximum allowed error in the magnitude response. |
| var maxAllowedMagError = 5.7e-7; |
| |
| // The maximum allowed error in the phase response. |
| var maxAllowedPhaseError = 4.7e-8; |
| |
| // The magnitudes and phases of the reference frequency response. |
| var magResponse; |
| var phaseResponse; |
| |
| // The magnitudes and phases of the reference frequency response. |
| var expectedMagnitudes; |
| var expectedPhases; |
| |
| // Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the |
| // Nyquist frequency. |
| function normalizedFrequency(freqHz, sampleRate) |
| { |
| var nyquist = sampleRate / 2; |
| return freqHz / nyquist; |
| } |
| |
| // Get the filter response at a (normalized) frequency |f| for the filter with coefficients |coef|. |
| function getResponseAt(coef, f) |
| { |
| var b0 = coef.b0; |
| var b1 = coef.b1; |
| var b2 = coef.b2; |
| var a1 = coef.a1; |
| var a2 = coef.a2; |
| |
| // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2) |
| // |
| // Compute H(exp(i * pi * f)). No native complex numbers in javascript, so break H(exp(i * pi * // f)) |
| // in to the real and imaginary parts of the numerator and denominator. Let omega = pi * f. |
| // Then the numerator is |
| // |
| // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) + b2 * sin(2 * omega)) |
| // |
| // and the denominator is |
| // |
| // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2 * sin(2 * omega)) |
| // |
| // Compute the magnitude and phase from the real and imaginary parts. |
| |
| var omega = Math.PI * f; |
| var numeratorReal = b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega); |
| var numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega)); |
| var denominatorReal = 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega); |
| var denominatorImag = -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega)); |
| |
| var magnitude = Math.sqrt((numeratorReal * numeratorReal + numeratorImag * numeratorImag) |
| / (denominatorReal * denominatorReal + denominatorImag * denominatorImag)); |
| var phase = Math.atan2(numeratorImag, numeratorReal) - Math.atan2(denominatorImag, denominatorReal); |
| |
| if (phase >= Math.PI) { |
| phase -= 2 * Math.PI; |
| } else if (phase <= -Math.PI) { |
| phase += 2 * Math.PI; |
| } |
| |
| return {magnitude : magnitude, phase : phase}; |
| } |
| |
| // Compute the reference frequency response for the biquad filter |filter| at the frequency samples |
| // given by |frequencies|. |
| function frequencyResponseReference(filter, frequencies) |
| { |
| var sampleRate = filter.context.sampleRate; |
| var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate); |
| var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value); |
| |
| var magnitudes = []; |
| var phases = []; |
| |
| for (var k = 0; k < frequencies.length; ++k) { |
| var response = getResponseAt(filterCoefficients, normalizedFrequency(frequencies[k], sampleRate)); |
| magnitudes.push(response.magnitude); |
| phases.push(response.phase); |
| } |
| |
| return {magnitudes : magnitudes, phases : phases}; |
| } |
| |
| // Compute a set of linearly spaced frequencies. |
| function createFrequencies(nFrequencies, sampleRate) |
| { |
| var frequencies = new Float32Array(nFrequencies); |
| var nyquist = sampleRate / 2; |
| var freqDelta = nyquist / nFrequencies; |
| |
| for (var k = 0; k < nFrequencies; ++k) { |
| frequencies[k] = k * freqDelta; |
| } |
| |
| return frequencies; |
| } |
| |
| function linearToDecibels(x) |
| { |
| if (x) { |
| return 20 * Math.log(x) / Math.LN10; |
| } else { |
| return -1000; |
| } |
| } |
| |
| // Look through the array and find any NaN or infinity. Returns the index of the first occurence or |
| // -1 if none. |
| function findBadNumber(signal) |
| { |
| for (var k = 0; k < signal.length; ++k) { |
| if (!isValidNumber(signal[k])) { |
| return k; |
| } |
| } |
| return -1; |
| } |
| |
| // Compute absolute value of the difference between phase angles, taking into account the wrapping |
| // of phases. |
| function absolutePhaseDifference(x, y) |
| { |
| var diff = Math.abs(x - y); |
| |
| if (diff > Math.PI) { |
| diff = 2 * Math.PI - diff; |
| } |
| return diff; |
| } |
| |
| // Compare the frequency response with our expected response. |
| function compareResponses(filter, frequencies, magResponse, phaseResponse) |
| { |
| var expectedResponse = frequencyResponseReference(filter, frequencies); |
| |
| expectedMagnitudes = expectedResponse.magnitudes; |
| expectedPhases = expectedResponse.phases; |
| |
| var n = magResponse.length; |
| var success = true; |
| var badResponse = false; |
| |
| var maxMagError = -1; |
| var maxMagErrorIndex = -1; |
| |
| var k; |
| var hasBadNumber; |
| |
| hasBadNumber = findBadNumber(magResponse); |
| if (hasBadNumber >= 0) { |
| testFailed("Magnitude response has NaN or infinity at " + hasBadNumber); |
| success = false; |
| badResponse = true; |
| } |
| |
| hasBadNumber = findBadNumber(phaseResponse); |
| if (hasBadNumber >= 0) { |
| testFailed("Phase response has NaN or infinity at " + hasBadNumber); |
| success = false; |
| badResponse = true; |
| } |
| |
| // These aren't testing the implementation itself. Instead, these are sanity checks on the |
| // reference. Failure here does not imply an error in the implementation. |
| hasBadNumber = findBadNumber(expectedMagnitudes); |
| if (hasBadNumber >= 0) { |
| testFailed("Expected magnitude response has NaN or infinity at " + hasBadNumber); |
| success = false; |
| badResponse = true; |
| } |
| |
| hasBadNumber = findBadNumber(expectedPhases); |
| if (hasBadNumber >= 0) { |
| testFailed("Expected phase response has NaN or infinity at " + hasBadNumber); |
| success = false; |
| badResponse = true; |
| } |
| |
| // If we found a NaN or infinity, the following tests aren't very helpful, especially for NaN. |
| // We run them anyway, after printing a warning message. |
| |
| if (badResponse) { |
| testFailed("NaN or infinity in the actual or expected results makes the following test results suspect."); |
| success = false; |
| } |
| |
| for (k = 0; k < n; ++k) { |
| var error = Math.abs(linearToDecibels(magResponse[k]) - linearToDecibels(expectedMagnitudes[k])); |
| if (error > maxMagError) { |
| maxMagError = error; |
| maxMagErrorIndex = k; |
| } |
| } |
| |
| if (maxMagError > maxAllowedMagError) { |
| var message = "Magnitude error (" + maxMagError + " dB)"; |
| message += " exceeded threshold at " + frequencies[maxMagErrorIndex]; |
| message += " Hz. Actual: " + linearToDecibels(magResponse[maxMagErrorIndex]); |
| message += " dB, expected: " + linearToDecibels(expectedMagnitudes[maxMagErrorIndex]) + " dB."; |
| testFailed(message); |
| success = false; |
| } else { |
| testPassed("Magnitude response within acceptable threshold."); |
| } |
| |
| var maxPhaseError = -1; |
| var maxPhaseErrorIndex = -1; |
| |
| for (k = 0; k < n; ++k) { |
| var error = absolutePhaseDifference(phaseResponse[k], expectedPhases[k]); |
| if (error > maxPhaseError) { |
| maxPhaseError = error; |
| maxPhaseErrorIndex = k; |
| } |
| } |
| |
| if (maxPhaseError > maxAllowedPhaseError) { |
| var message = "Phase error (radians) (" + maxPhaseError; |
| message += ") exceeded threshold at " + frequencies[maxPhaseErrorIndex]; |
| message += " Hz. Actual: " + phaseResponse[maxPhaseErrorIndex]; |
| message += " expected: " + expectedPhases[maxPhaseErrorIndex]; |
| testFailed(message); |
| success = false; |
| } else { |
| testPassed("Phase response within acceptable threshold."); |
| } |
| |
| |
| return success; |
| } |
| |
| function runTest() |
| { |
| window.jsTestIsAsync = true; |
| |
| context = new webkitAudioContext(); |
| |
| filter = context.createBiquadFilter(); |
| |
| // Arbitrarily test a peaking filter, but any kind of filter can be tested. |
| filter.type = "peaking"; |
| filter.frequency.value = filterCutoff; |
| filter.Q.value = filterQ; |
| filter.gain.value = filterGain; |
| |
| var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate); |
| magResponse = new Float32Array(numberOfFrequencies); |
| phaseResponse = new Float32Array(numberOfFrequencies); |
| |
| filter.getFrequencyResponse(frequencies, magResponse, phaseResponse); |
| var success = compareResponses(filter, frequencies, magResponse, phaseResponse); |
| |
| if (success) { |
| testPassed("Frequency response was correct."); |
| } else { |
| testFailed("Frequency response was incorrect."); |
| } |
| |
| finishJSTest(); |
| } |
| |
| runTest(); |
| |
| </script> |
| </body> |
| </html> |