diff options
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html')
-rw-r--r-- | testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html new file mode 100644 index 0000000000..23222e4df9 --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html @@ -0,0 +1,394 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test BiquadFilter getFrequencyResponse() functionality + </title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/webaudio/resources/audit-util.js"></script> + <script src="/webaudio/resources/audit.js"></script> + <script src="/webaudio/resources/biquad-filters.js"></script> + <script src="/webaudio/resources/biquad-testing.js"></script> + </head> + <body> + <script id="layout-test-code"> + let audit = Audit.createTaskRunner(); + + // 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. + + // The magnitude response of the biquad filter. + let magResponse; + + // The phase response of the biquad filter. + let phaseResponse; + + // Number of frequency samples to take. + let numberOfFrequencies = 1000; + + // The filter parameters. + let filterCutoff = 1000; // Hz. + let filterQ = 1; + let filterGain = 5; // Decibels. + + // The magnitudes and phases of the reference frequency response. + let expectedMagnitudes; + let expectedPhases; + + // Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 + // corresponding to the Nyquist frequency. + function normalizedFrequency(freqHz, sampleRate) { + let 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) { + let b0 = coef.b0; + let b1 = coef.b1; + let b2 = coef.b2; + let a1 = coef.a1; + let 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. + + let omega = Math.PI * f; + let numeratorReal = + b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega); + let numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega)); + let denominatorReal = + 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega); + let denominatorImag = + -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega)); + + let magnitude = Math.sqrt( + (numeratorReal * numeratorReal + numeratorImag * numeratorImag) / + (denominatorReal * denominatorReal + + denominatorImag * denominatorImag)); + let 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) { + let sampleRate = filter.context.sampleRate; + let normalizedFreq = + normalizedFrequency(filter.frequency.value, sampleRate); + let filterCoefficients = createFilter( + filter.type, normalizedFreq, filter.Q.value, filter.gain.value); + + let magnitudes = []; + let phases = []; + + for (let k = 0; k < frequencies.length; ++k) { + let 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) { + let frequencies = new Float32Array(nFrequencies); + let nyquist = sampleRate / 2; + let freqDelta = nyquist / nFrequencies; + + for (let 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; + } + } + + function decibelsToLinear(x) { + return Math.pow(10, x/20); + } + + // 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 (let 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) { + let diff = Math.abs(x - y); + + if (diff > Math.PI) { + diff = 2 * Math.PI - diff; + } + return diff; + } + + // Compare the frequency response with our expected response. + // + // should - The |should| method provided by audit.define + // filter - The filter node used in the test + // frequencies - array of frequencies provided to |getFrequencyResponse| + // magResponse - mag response from |getFrequencyResponse| + // phaseResponse - phase response from |getFrequencyResponse| + // maxAllowedMagError - error threshold for mag response, in dB + // maxAllowedPhaseError - error threshold for phase response, in rad. + function compareResponses( + should, filter, frequencies, magResponse, phaseResponse, + maxAllowedMagError, maxAllowedPhaseError) { + let expectedResponse = frequencyResponseReference(filter, frequencies); + + expectedMagnitudes = expectedResponse.magnitudes; + expectedPhases = expectedResponse.phases; + + let n = magResponse.length; + let badResponse = false; + + let maxMagError = -1; + let maxMagErrorIndex = -1; + + let k; + let hasBadNumber; + + hasBadNumber = findBadNumber(magResponse); + badResponse = + !should( + hasBadNumber >= 0 ? 1 : 0, + filter.type + + ': Number of non-finite values in magnitude response') + .beEqualTo(0); + + hasBadNumber = findBadNumber(phaseResponse); + badResponse = + !should( + hasBadNumber >= 0 ? 1 : 0, + filter.type + ': Number of non-finte values in phase response') + .beEqualTo(0); + + // 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); + badResponse = + !should( + hasBadNumber >= 0 ? 1 : 0, + filter.type + + ': Number of non-finite values in the expected magnitude response') + .beEqualTo(0); + + hasBadNumber = findBadNumber(expectedPhases); + badResponse = + !should( + hasBadNumber >= 0 ? 1 : 0, + filter.type + + ': Number of non-finite values in expected phase response') + .beEqualTo(0); + + // 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. + should( + !badResponse, + filter.type + + ': Actual and expected results contained only finite values') + .beTrue(); + + for (k = 0; k < n; ++k) { + let error = Math.abs( + linearToDecibels(magResponse[k]) - + linearToDecibels(expectedMagnitudes[k])); + if (error > maxMagError) { + maxMagError = error; + maxMagErrorIndex = k; + } + } + + should( + linearToDecibels(maxMagError), + filter.type + ': Max error (' + linearToDecibels(maxMagError) + + ' dB) of magnitude response at frequency ' + + frequencies[maxMagErrorIndex] + ' Hz') + .beLessThanOrEqualTo(linearToDecibels(maxAllowedMagError)); + let maxPhaseError = -1; + let maxPhaseErrorIndex = -1; + + for (k = 0; k < n; ++k) { + let error = + absolutePhaseDifference(phaseResponse[k], expectedPhases[k]); + if (error > maxPhaseError) { + maxPhaseError = error; + maxPhaseErrorIndex = k; + } + } + + should( + radToDegree(maxPhaseError), + filter.type + ': Max error (' + radToDegree(maxPhaseError) + + ' deg) in phase response at frequency ' + + frequencies[maxPhaseErrorIndex] + ' Hz') + .beLessThanOrEqualTo(radToDegree(maxAllowedPhaseError)); + } + + function radToDegree(rad) { + // Radians to degrees + return rad * 180 / Math.PI; + } + + // Test the getFrequencyResponse for each of filter types. Each entry in + // this array is a dictionary with these elements: + // + // type: filter type to be tested + // maxErrorInMagnitude: Allowed error in computed magnitude response + // maxErrorInPhase: Allowed error in computed magnitude phase + [{ + type: 'lowpass', + maxErrorInMagnitude: decibelsToLinear(-73.0178), + maxErrorInPhase: 8.04360e-6 + }, + { + type: 'highpass', + maxErrorInMagnitude: decibelsToLinear(-117.5461), + maxErrorInPhase: 6.9691e-6 + }, + { + type: 'bandpass', + maxErrorInMagnitude: decibelsToLinear(-79.0139), + maxErrorInPhase: 4.9371e-6 + }, + { + type: 'lowshelf', + maxErrorInMagnitude: decibelsToLinear(-120.4038), + maxErrorInPhase: 4.0724e-6 + }, + { + type: 'highshelf', + maxErrorInMagnitude: decibelsToLinear(-120, 1303), + maxErrorInPhase: 4.0724e-6 + }, + { + type: 'peaking', + maxErrorInMagnitude: decibelsToLinear(-119.1176), + maxErrorInPhase: 6.4724e-8 + }, + { + type: 'notch', + maxErrorInMagnitude: decibelsToLinear(-87.0808), + maxErrorInPhase: 6.6300e-6 + }, + { + type: 'allpass', + maxErrorInMagnitude: decibelsToLinear(-265.3517), + maxErrorInPhase: 1.3260e-5 + }].forEach(test => { + audit.define( + {label: test.type, description: 'Frequency response'}, + (task, should) => { + let context = new AudioContext(); + + let filter = new BiquadFilterNode(context, { + type: test.type, + frequency: filterCutoff, + Q: filterQ, + gain: filterGain + }); + + let frequencies = + createFrequencies(numberOfFrequencies, context.sampleRate); + magResponse = new Float32Array(numberOfFrequencies); + phaseResponse = new Float32Array(numberOfFrequencies); + + filter.getFrequencyResponse( + frequencies, magResponse, phaseResponse); + compareResponses( + should, filter, frequencies, magResponse, phaseResponse, + test.maxErrorInMagnitude, test.maxErrorInPhase); + + task.done(); + }); + }); + + audit.define( + { + label: 'getFrequencyResponse', + description: 'Test out-of-bounds frequency values' + }, + (task, should) => { + let context = new OfflineAudioContext(1, 1, sampleRate); + let filter = new BiquadFilterNode(context); + + // Frequencies to test. These are all outside the valid range of + // frequencies of 0 to Nyquist. + let freq = new Float32Array(2); + freq[0] = -1; + freq[1] = context.sampleRate / 2 + 1; + + let mag = new Float32Array(freq.length); + let phase = new Float32Array(freq.length); + + filter.getFrequencyResponse(freq, mag, phase); + + // Verify that the returned magnitude and phase entries are alL NaN + // since the frequencies are outside the valid range + for (let k = 0; k < mag.length; ++k) { + should(mag[k], + 'Magnitude response at frequency ' + freq[k]) + .beNaN(); + } + + for (let k = 0; k < phase.length; ++k) { + should(phase[k], + 'Phase response at frequency ' + freq[k]) + .beNaN(); + } + + task.done(); + }); + + audit.run(); + </script> + </body> +</html> |