// 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); }