blob: 1028f64ddbdf3e9ae777d51b1e7bf981ad2bcd0a [file] [log] [blame]
<!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>