172 lines
5.4 KiB
JavaScript
172 lines
5.4 KiB
JavaScript
// Globals, to make testing and debugging easier.
|
|
let context;
|
|
let filter;
|
|
let signal;
|
|
let renderedBuffer;
|
|
let renderedData;
|
|
|
|
// Use a power of two to eliminate round-off in converting frame to time
|
|
let sampleRate = 32768;
|
|
let pulseLengthFrames = .1 * sampleRate;
|
|
|
|
// Maximum allowed error for the test to succeed. Experimentally determined.
|
|
let maxAllowedError = 5.9e-8;
|
|
|
|
// This must be large enough so that the filtered result is essentially zero.
|
|
// See comments for createTestAndRun. This must be a whole number of frames.
|
|
let timeStep = Math.ceil(.1 * sampleRate) / sampleRate;
|
|
|
|
// Maximum number of filters we can process (mostly for setting the
|
|
// render length correctly.)
|
|
let maxFilters = 5;
|
|
|
|
// How long to render. Must be long enough for all of the filters we
|
|
// want to test.
|
|
let renderLengthSeconds = timeStep * (maxFilters + 1);
|
|
|
|
let renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);
|
|
|
|
// Number of filters that will be processed.
|
|
let nFilters;
|
|
|
|
function createImpulseBuffer(context, length) {
|
|
let impulse = context.createBuffer(1, length, context.sampleRate);
|
|
let data = impulse.getChannelData(0);
|
|
for (let k = 1; k < data.length; ++k) {
|
|
data[k] = 0;
|
|
}
|
|
data[0] = 1;
|
|
|
|
return impulse;
|
|
}
|
|
|
|
|
|
function createTestAndRun(context, filterType, testParameters) {
|
|
// 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.
|
|
|
|
let filterParameters = testParameters.filterParameters;
|
|
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 (let 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].start(timeStep * k);
|
|
}
|
|
|
|
return context.startRendering().then(buffer => {
|
|
checkFilterResponse(buffer, filterType, testParameters);
|
|
});
|
|
}
|
|
|
|
function addSignal(dest, src, destOffset) {
|
|
// Add src to dest at the given dest offset.
|
|
for (let k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
|
|
dest[k] += src[j];
|
|
}
|
|
}
|
|
|
|
function generateReference(filterType, filterParameters) {
|
|
let result = new Array(renderLengthSamples);
|
|
let data = new Array(renderLengthSamples);
|
|
// Initialize the result array and data.
|
|
for (let k = 0; k < result.length; ++k) {
|
|
result[k] = 0;
|
|
data[k] = 0;
|
|
}
|
|
// Make data an impulse.
|
|
data[0] = 1;
|
|
|
|
for (let k = 0; k < nFilters; ++k) {
|
|
// Filter an impulse
|
|
let detune = (filterParameters[k].detune === undefined) ?
|
|
0 :
|
|
filterParameters[k].detune;
|
|
let frequency = filterParameters[k].cutoff *
|
|
Math.pow(2, detune / 1200); // Apply detune, converting from Cents.
|
|
|
|
let filterCoef = createFilter(
|
|
filterType, frequency, filterParameters[k].q, filterParameters[k].gain);
|
|
let 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(renderedBuffer, filterType, testParameters) {
|
|
let filterParameters = testParameters.filterParameters;
|
|
let maxAllowedError = testParameters.threshold;
|
|
let should = testParameters.should;
|
|
|
|
renderedData = renderedBuffer.getChannelData(0);
|
|
|
|
reference = generateReference(filterType, filterParameters);
|
|
|
|
let len = Math.min(renderedData.length, reference.length);
|
|
|
|
let success = true;
|
|
|
|
// Maximum error between rendered data and expected data
|
|
let maxError = 0;
|
|
|
|
// Sample offset where the maximum error occurred.
|
|
let maxPosition = 0;
|
|
|
|
// Number of infinities or NaNs that occurred in the rendered data.
|
|
let invalidNumberCount = 0;
|
|
|
|
should(nFilters, 'Number of filters tested')
|
|
.beEqualTo(filterParameters.length);
|
|
|
|
// 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 (let k = 0; k < len; ++k) {
|
|
let err = Math.abs(renderedData[k] - reference[k]);
|
|
if (err > maxError) {
|
|
maxError = err;
|
|
maxPosition = k;
|
|
}
|
|
if (!isValidNumber(renderedData[k])) {
|
|
++invalidNumberCount;
|
|
}
|
|
}
|
|
|
|
should(
|
|
invalidNumberCount, 'Number of non-finite values in the rendered output')
|
|
.beEqualTo(0);
|
|
|
|
should(maxError, 'Max error in ' + filterTypeName[filterType] + ' response')
|
|
.beLessThanOrEqualTo(maxAllowedError);
|
|
}
|