blob: 027af11a977336f38a4677011369217cde8a49d3 [file] [log] [blame]
// Globals, to make testing and debugging easier.
var context;
var filter;
var signal;
var renderedBuffer;
var renderedData;
var sampleRate = 44100.0;
var pulseLengthFrames = .1 * sampleRate;
// Maximum allowed error for the test to succeed. Experimentally determined.
var maxAllowedError = 5.9e-8;
// This must be large enough so that the filtered result is
// essentially zero. See comments for createTestAndRun.
var timeStep = .1;
// Maximum number of filters we can process (mostly for setting the
// render length correctly.)
var maxFilters = 5;
// How long to render. Must be long enough for all of the filters we
// want to test.
var renderLengthSeconds = timeStep * (maxFilters + 1) ;
var renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);
// Number of filters that will be processed.
var nFilters;
// A biquad filter has a z-transform of
// H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
//
// The formulas for the various filters were taken from
// http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt.
// Lowpass filter.
function createLowpassFilter(freq, q, gain) {
var b0;
var b1;
var b2;
var a1;
var a2;
if (freq == 1) {
// The formula below works, except for roundoff. When freq = 1,
// the filter is just a wire, so hardwire the coefficients.
b0 = 1;
b1 = 0;
b2 = 0;
a1 = 0;
a2 = 0;
} else {
var g = Math.pow(10, q / 20);
var d = Math.sqrt((4 - Math.sqrt(16 - 16 / (g * g))) / 2);
var theta = Math.PI * freq;
var sn = d * Math.sin(theta) / 2;
var beta = 0.5 * (1 - sn) / (1 + sn);
var gamma = (0.5 + beta) * Math.cos(theta);
var alpha = 0.25 * (0.5 + beta - gamma);
b0 = 2 * alpha;
b1 = 4 * alpha;
b2 = 2 * alpha;
a1 = 2 * (-gamma);
a2 = 2 * beta;
}
return {b0 : b0, b1 : b1, b2 : b2, a1 : a1, a2 : a2};
}
function createHighpassFilter(freq, q, gain) {
var b0;
var b1;
var b2;
var a1;
var a2;
if (freq == 1) {
// The filter is 0
b0 = 0;
b1 = 0;
b2 = 0;
a1 = 0;
a2 = 0;
} else if (freq == 0) {
// The filter is 1. Computation of coefficients below is ok, but
// there's a pole at 1 and a zero at 1, so round-off could make
// the filter unstable.
b0 = 1;
b1 = 0;
b2 = 0;
a1 = 0;
a2 = 0;
} else {
var g = Math.pow(10, q / 20);
var d = Math.sqrt((4 - Math.sqrt(16 - 16 / (g * g))) / 2);
var theta = Math.PI * freq;
var sn = d * Math.sin(theta) / 2;
var beta = 0.5 * (1 - sn) / (1 + sn);
var gamma = (0.5 + beta) * Math.cos(theta);
var alpha = 0.25 * (0.5 + beta + gamma);
b0 = 2 * alpha;
b1 = -4 * alpha;
b2 = 2 * alpha;
a1 = 2 * (-gamma);
a2 = 2 * beta;
}
return {b0 : b0, b1 : b1, b2 : b2, a1 : a1, a2 : a2};
}
function normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2) {
var scale = 1 / a0;
return {b0 : b0 * scale,
b1 : b1 * scale,
b2 : b2 * scale,
a1 : a1 * scale,
a2 : a2 * scale};
}
function createBandpassFilter(freq, q, gain) {
var b0;
var b1;
var b2;
var a0;
var a1;
var a2;
var coef;
if (freq > 0 && freq < 1) {
var w0 = Math.PI * freq;
if (q > 0) {
var alpha = Math.sin(w0) / (2 * q);
var k = Math.cos(w0);
b0 = alpha;
b1 = 0;
b2 = -alpha;
a0 = 1 + alpha;
a1 = -2 * k;
a2 = 1 - alpha;
coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
} else {
// q = 0, and frequency is not 0 or 1. The above formula has a
// divide by zero problem. The limit of the z-transform as q
// approaches 0 is 1, so set the filter that way.
coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
} else {
// When freq = 0 or 1, the z-transform is identically 0,
// independent of q.
coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0}
}
return coef;
}
function createLowShelfFilter(freq, q, gain) {
// q not used
var b0;
var b1;
var b2;
var a0;
var a1;
var a2;
var coef;
var S = 1;
var A = Math.pow(10, gain / 40);
if (freq == 1) {
// The filter is just a constant gain
coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
} else if (freq == 0) {
// The filter is 1
coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
} else {
var w0 = Math.PI * freq;
var alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
var k = Math.cos(w0);
var k2 = 2 * Math.sqrt(A) * alpha;
var Ap1 = A + 1;
var Am1 = A - 1;
b0 = A * (Ap1 - Am1 * k + k2);
b1 = 2 * A * (Am1 - Ap1 * k);
b2 = A * (Ap1 - Am1 * k - k2);
a0 = Ap1 + Am1 * k + k2;
a1 = -2 * (Am1 + Ap1 * k);
a2 = Ap1 + Am1 * k - k2;
coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
}
return coef;
}
function createHighShelfFilter(freq, q, gain) {
// q not used
var b0;
var b1;
var b2;
var a0;
var a1;
var a2;
var coef;
var A = Math.pow(10, gain / 40);
if (freq == 1) {
// When freq = 1, the z-transform is 1
coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
} else if (freq > 0) {
var w0 = Math.PI * freq;
var S = 1;
var alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
var k = Math.cos(w0);
var k2 = 2 * Math.sqrt(A) * alpha;
var Ap1 = A + 1;
var Am1 = A - 1;
b0 = A * (Ap1 + Am1 * k + k2);
b1 = -2 * A * (Am1 + Ap1 * k);
b2 = A * (Ap1 + Am1 * k - k2);
a0 = Ap1 - Am1 * k + k2;
a1 = 2 * (Am1 - Ap1*k);
a2 = Ap1 - Am1 * k-k2;
coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
} else {
// When freq = 0, the filter is just a gain
coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
return coef;
}
function createPeakingFilter(freq, q, gain) {
var b0;
var b1;
var b2;
var a0;
var a1;
var a2;
var coef;
var A = Math.pow(10, gain / 40);
if (freq > 0 && freq < 1) {
if (q > 0) {
var w0 = Math.PI * freq;
var alpha = Math.sin(w0) / (2 * q);
var k = Math.cos(w0);
b0 = 1 + alpha * A;
b1 = -2 * k;
b2 = 1 - alpha * A;
a0 = 1 + alpha / A;
a1 = -2 * k;
a2 = 1 - alpha / A;
coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
} else {
// q = 0, we have a divide by zero problem in the formulas
// above. But if we look at the z-transform, we see that the
// limit as q approaches 0 is A^2.
coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
} else {
// freq = 0 or 1, the z-transform is 1
coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
return coef;
}
function createNotchFilter(freq, q, gain) {
var b0;
var b1;
var b2;
var a0;
var a1;
var a2;
var coef;
if (freq > 0 && freq < 1) {
if (q > 0) {
var w0 = Math.PI * freq;
var alpha = Math.sin(w0) / (2 * q);
var k = Math.cos(w0);
b0 = 1;
b1 = -2 * k;
b2 = 1;
a0 = 1 + alpha;
a1 = -2 * k;
a2 = 1 - alpha;
coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
} else {
// When q = 0, we get a divide by zero above. The limit of the
// z-transform as q approaches 0 is 0, so set the coefficients
// appropriately.
coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
} else {
// When freq = 0 or 1, the z-transform is 1
coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
return coef;
}
function createAllpassFilter(freq, q, gain) {
var b0;
var b1;
var b2;
var a0;
var a1;
var a2;
var coef;
if (freq > 0 && freq < 1) {
if (q > 0) {
var w0 = Math.PI * freq;
var alpha = Math.sin(w0) / (2 * q);
var k = Math.cos(w0);
b0 = 1 - alpha;
b1 = -2 * k;
b2 = 1 + alpha;
a0 = 1 + alpha;
a1 = -2 * k;
a2 = 1 - alpha;
coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
} else {
// q = 0
coef = {b0 : -1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
} else {
coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
}
return coef;
}
// Array of functions to compute the filter coefficients. This must
// be arraned in the same order as the filter types in the idl file.
var filterCreatorFunction = [createLowpassFilter,
createHighpassFilter,
createBandpassFilter,
createLowShelfFilter,
createHighShelfFilter,
createPeakingFilter,
createNotchFilter,
createAllpassFilter];
var filterTypeName = ["Lowpass filter",
"Highpass filter",
"Bandpass filter",
"Lowshelf filter",
"Highshelf filter",
"Peaking filter",
"Notch filter",
"Allpass filter"];
var filterTypeIndex = {"lowpass": 0,
"highpass": 1,
"bandpass": 2,
"lowshelf": 3,
"highshelf": 4,
"peaking": 5,
"notch": 6,
"allpass": 7};
function createFilter(filterType, freq, q, gain) {
return filterCreatorFunction[filterType](freq, q, gain);
}
function filterData(filterCoef, signal, len) {
var y = new Array(len);
var b0 = filterCoef.b0;
var b1 = filterCoef.b1;
var b2 = filterCoef.b2;
var a1 = filterCoef.a1;
var a2 = filterCoef.a2;
// Prime the pump. (Assumes the signal has length >= 2!)
y[0] = b0 * signal[0];
y[1] = b0 * signal[1] + b1 * signal[0] - a1 * y[0];
// Filter all of the signal that we have.
for (var k = 2; k < Math.min(signal.length, len); ++k) {
y[k] = b0 * signal[k] + b1 * signal[k-1] + b2 * signal[k-2] - a1 * y[k-1] - a2 * y[k-2];
}
// If we need to filter more, but don't have any signal left,
// assume the signal is zero.
for (var k = signal.length; k < len; ++k) {
y[k] = - a1 * y[k-1] - a2 * y[k-2];
}
return y;
}
function createImpulseBuffer(context, length) {
var impulse = context.createBuffer(1, length, context.sampleRate);
var data = impulse.getChannelData(0);
for (var k = 1; k < data.length; ++k) {
data[k] = 0;
}
data[0] = 1;
return impulse;
}
function createTestAndRun(context, filterType, filterParameters) {
// To test the filters, we apply a signal (an impulse) to each of
// the specified filters, with each signal starting at a different
// time. The output of the filters is summed together at the
// output. Thus for filter k, the signal input to the filter
// starts at time k * timeStep. For this to work well, timeStep
// must be large enough for the output of each filter to have
// decayed to zero with timeStep seconds. That way the filter
// outputs don't interfere with each other.
nFilters = Math.min(filterParameters.length, maxFilters);
signal = new Array(nFilters);
filter = new Array(nFilters);
impulse = createImpulseBuffer(context, pulseLengthFrames);
// Create all of the signal sources and filters that we need.
for (var k = 0; k < nFilters; ++k) {
signal[k] = context.createBufferSource();
signal[k].buffer = impulse;
filter[k] = context.createBiquadFilter();
filter[k].type = filterType;
filter[k].frequency.value = context.sampleRate / 2 * filterParameters[k].cutoff;
filter[k].detune.value = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
filter[k].Q.value = filterParameters[k].q;
filter[k].gain.value = filterParameters[k].gain;
signal[k].connect(filter[k]);
filter[k].connect(context.destination);
signal[k].noteOn(timeStep * k);
}
context.oncomplete = checkFilterResponse(filterType, filterParameters);
context.startRendering();
}
function addSignal(dest, src, destOffset) {
// Add src to dest at the given dest offset.
for (var k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
dest[k] += src[j];
}
}
function generateReference(filterType, filterParameters) {
var result = new Array(renderLengthSamples);
var data = new Array(renderLengthSamples);
// Initialize the result array and data.
for (var k = 0; k < result.length; ++k) {
result[k] = 0;
data[k] = 0;
}
// Make data an impulse.
data[0] = 1;
for (var k = 0; k < nFilters; ++k) {
// Filter an impulse
var detune = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
var frequency = filterParameters[k].cutoff * Math.pow(2, detune / 1200); // Apply detune, converting from Cents.
var filterCoef = createFilter(filterType,
frequency,
filterParameters[k].q,
filterParameters[k].gain);
var y = filterData(filterCoef, data, renderLengthSamples);
// Accumulate this filtered data into the final output at the desired offset.
addSignal(result, y, timeToSampleFrame(timeStep * k, sampleRate));
}
return result;
}
function checkFilterResponse(filterType, filterParameters) {
return function(event) {
renderedBuffer = event.renderedBuffer;
renderedData = renderedBuffer.getChannelData(0);
reference = generateReference(filterType, filterParameters);
var len = Math.min(renderedData.length, reference.length);
var success = true;
// Maximum error between rendered data and expected data
var maxError = 0;
// Sample offset where the maximum error occurred.
var maxPosition = 0;
// Number of infinities or NaNs that occurred in the rendered data.
var invalidNumberCount = 0;
if (nFilters != filterParameters.length) {
testFailed("Test wanted " + filterParameters.length + " filters but only " + maxFilters + " allowed.");
success = false;
}
// Compare the rendered signal with our reference, keeping
// track of the maximum difference (and the offset of the max
// difference.) Check for bad numbers in the rendered output
// too. There shouldn't be any.
for (var k = 0; k < len; ++k) {
var err = Math.abs(renderedData[k] - reference[k]);
if (err > maxError) {
maxError = err;
maxPosition = k;
}
if (!isValidNumber(renderedData[k])) {
++invalidNumberCount;
}
}
if (invalidNumberCount > 0) {
testFailed("Rendered output has " + invalidNumberCount + " infinities or NaNs.");
success = false;
} else {
testPassed("Rendered output did not have infinities or NaNs.");
}
if (maxError <= maxAllowedError) {
testPassed(filterTypeName[filterType] + " response is correct.");
} else {
testFailed(filterTypeName[filterType] + " response is incorrect. Max err = " + maxError + " at " + maxPosition + ". Threshold = " + maxAllowedError);
success = false;
}
if (success) {
testPassed("Test signal was correctly filtered.");
} else {
testFailed("Test signal was not correctly filtered.");
}
finishJSTest();
}
}