biquad-testing.js (5545B)
1 // Globals, to make testing and debugging easier. 2 let context; 3 let filter; 4 let signal; 5 let renderedBuffer; 6 let renderedData; 7 8 // Use a power of two to eliminate round-off in converting frame to time 9 let sampleRate = 32768; 10 let pulseLengthFrames = .1 * sampleRate; 11 12 // Maximum allowed error for the test to succeed. Experimentally determined. 13 let maxAllowedError = 5.9e-8; 14 15 // This must be large enough so that the filtered result is essentially zero. 16 // See comments for createTestAndRun. This must be a whole number of frames. 17 let timeStep = Math.ceil(.1 * sampleRate) / sampleRate; 18 19 // Maximum number of filters we can process (mostly for setting the 20 // render length correctly.) 21 let maxFilters = 5; 22 23 // How long to render. Must be long enough for all of the filters we 24 // want to test. 25 let renderLengthSeconds = timeStep * (maxFilters + 1); 26 27 let renderLengthSamples = Math.round(renderLengthSeconds * sampleRate); 28 29 // Number of filters that will be processed. 30 let nFilters; 31 32 function createImpulseBuffer(context, length) { 33 let impulse = context.createBuffer(1, length, context.sampleRate); 34 let data = impulse.getChannelData(0); 35 for (let k = 1; k < data.length; ++k) { 36 data[k] = 0; 37 } 38 data[0] = 1; 39 40 return impulse; 41 } 42 43 44 function createTestAndRun(context, filterType, testParameters) { 45 // To test the filters, we apply a signal (an impulse) to each of 46 // the specified filters, with each signal starting at a different 47 // time. The output of the filters is summed together at the 48 // output. Thus for filter k, the signal input to the filter 49 // starts at time k * timeStep. For this to work well, timeStep 50 // must be large enough for the output of each filter to have 51 // decayed to zero with timeStep seconds. That way the filter 52 // outputs don't interfere with each other. 53 54 let filterParameters = testParameters.filterParameters; 55 nFilters = Math.min(filterParameters.length, maxFilters); 56 57 signal = new Array(nFilters); 58 filter = new Array(nFilters); 59 60 impulse = createImpulseBuffer(context, pulseLengthFrames); 61 62 // Create all of the signal sources and filters that we need. 63 for (let k = 0; k < nFilters; ++k) { 64 signal[k] = context.createBufferSource(); 65 signal[k].buffer = impulse; 66 67 filter[k] = context.createBiquadFilter(); 68 filter[k].type = filterType; 69 filter[k].frequency.value = 70 context.sampleRate / 2 * filterParameters[k].cutoff; 71 filter[k].detune.value = (filterParameters[k].detune === undefined) ? 72 0 : 73 filterParameters[k].detune; 74 filter[k].Q.value = filterParameters[k].q; 75 filter[k].gain.value = filterParameters[k].gain; 76 77 signal[k].connect(filter[k]); 78 filter[k].connect(context.destination); 79 80 signal[k].start(timeStep * k); 81 } 82 83 return context.startRendering().then(buffer => { 84 checkFilterResponse(buffer, filterType, testParameters); 85 }); 86 } 87 88 function addSignal(dest, src, destOffset) { 89 // Add src to dest at the given dest offset. 90 for (let k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) { 91 dest[k] += src[j]; 92 } 93 } 94 95 function generateReference(filterType, filterParameters) { 96 let result = new Array(renderLengthSamples); 97 let data = new Array(renderLengthSamples); 98 // Initialize the result array and data. 99 for (let k = 0; k < result.length; ++k) { 100 result[k] = 0; 101 data[k] = 0; 102 } 103 // Make data an impulse. 104 data[0] = 1; 105 106 for (let k = 0; k < nFilters; ++k) { 107 // Filter an impulse 108 let detune = (filterParameters[k].detune === undefined) ? 109 0 : 110 filterParameters[k].detune; 111 let frequency = filterParameters[k].cutoff * 112 Math.pow(2, detune / 1200); // Apply detune, converting from Cents. 113 114 let filterCoef = createFilter( 115 filterType, frequency, filterParameters[k].q, filterParameters[k].gain); 116 let y = filterData(filterCoef, data, renderLengthSamples); 117 118 // Accumulate this filtered data into the final output at the desired 119 // offset. 120 addSignal(result, y, timeToSampleFrame(timeStep * k, sampleRate)); 121 } 122 123 return result; 124 } 125 126 function checkFilterResponse(renderedBuffer, filterType, testParameters) { 127 let filterParameters = testParameters.filterParameters; 128 let maxAllowedError = testParameters.threshold; 129 let should = testParameters.should; 130 131 renderedData = renderedBuffer.getChannelData(0); 132 133 reference = generateReference(filterType, filterParameters); 134 135 let len = Math.min(renderedData.length, reference.length); 136 137 let success = true; 138 139 // Maximum error between rendered data and expected data 140 let maxError = 0; 141 142 // Sample offset where the maximum error occurred. 143 let maxPosition = 0; 144 145 // Number of infinities or NaNs that occurred in the rendered data. 146 let invalidNumberCount = 0; 147 148 should(nFilters, 'Number of filters tested') 149 .beEqualTo(filterParameters.length); 150 151 // Compare the rendered signal with our reference, keeping 152 // track of the maximum difference (and the offset of the max 153 // difference.) Check for bad numbers in the rendered output 154 // too. There shouldn't be any. 155 for (let k = 0; k < len; ++k) { 156 let err = Math.abs(renderedData[k] - reference[k]); 157 if (err > maxError) { 158 maxError = err; 159 maxPosition = k; 160 } 161 if (!isValidNumber(renderedData[k])) { 162 ++invalidNumberCount; 163 } 164 } 165 166 should( 167 invalidNumberCount, 'Number of non-finite values in the rendered output') 168 .beEqualTo(0); 169 170 should(maxError, 'Max error in ' + filterTypeName[filterType] + ' response') 171 .beLessThanOrEqualTo(maxAllowedError); 172 }