1 // Globals, to make testing and debugging easier.
8 var sampleRate = 44100.0;
9 var pulseLengthFrames = .1 * sampleRate;
11 // Maximum allowed error for the test to succeed. Experimentally determined.
12 var maxAllowedError = 5.9e-8;
14 // This must be large enough so that the filtered result is
15 // essentially zero. See comments for createTestAndRun.
18 // Maximum number of filters we can process (mostly for setting the
19 // render length correctly.)
22 // How long to render. Must be long enough for all of the filters we
24 var renderLengthSeconds = timeStep * (maxFilters + 1) ;
26 var renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);
28 // Number of filters that will be processed.
31 // A biquad filter has a z-transform of
32 // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
34 // The formulas for the various filters were taken from
35 // http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt.
39 function createLowpassFilter(freq, q, gain) {
47 // The formula below works, except for roundoff. When freq = 1,
48 // the filter is just a wire, so hardwire the coefficients.
55 var g = Math.pow(10, q / 20);
56 var d = Math.sqrt((4 - Math.sqrt(16 - 16 / (g * g))) / 2);
57 var theta = Math.PI * freq;
58 var sn = d * Math.sin(theta) / 2;
59 var beta = 0.5 * (1 - sn) / (1 + sn);
60 var gamma = (0.5 + beta) * Math.cos(theta);
61 var alpha = 0.25 * (0.5 + beta - gamma);
70 return {b0 : b0, b1 : b1, b2 : b2, a1 : a1, a2 : a2};
73 function createHighpassFilter(freq, q, gain) {
87 } else if (freq == 0) {
88 // The filter is 1. Computation of coefficients below is ok, but
89 // there's a pole at 1 and a zero at 1, so round-off could make
90 // the filter unstable.
97 var g = Math.pow(10, q / 20);
98 var d = Math.sqrt((4 - Math.sqrt(16 - 16 / (g * g))) / 2);
99 var theta = Math.PI * freq;
100 var sn = d * Math.sin(theta) / 2;
101 var beta = 0.5 * (1 - sn) / (1 + sn);
102 var gamma = (0.5 + beta) * Math.cos(theta);
103 var alpha = 0.25 * (0.5 + beta + gamma);
112 return {b0 : b0, b1 : b1, b2 : b2, a1 : a1, a2 : a2};
115 function normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2) {
118 return {b0 : b0 * scale,
125 function createBandpassFilter(freq, q, gain) {
134 if (freq > 0 && freq < 1) {
135 var w0 = Math.PI * freq;
137 var alpha = Math.sin(w0) / (2 * q);
138 var k = Math.cos(w0);
147 coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
149 // q = 0, and frequency is not 0 or 1. The above formula has a
150 // divide by zero problem. The limit of the z-transform as q
151 // approaches 0 is 1, so set the filter that way.
152 coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
155 // When freq = 0 or 1, the z-transform is identically 0,
157 coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0}
163 function createLowShelfFilter(freq, q, gain) {
174 var A = Math.pow(10, gain / 40);
177 // The filter is just a constant gain
178 coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
179 } else if (freq == 0) {
181 coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
183 var w0 = Math.PI * freq;
184 var alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
185 var k = Math.cos(w0);
186 var k2 = 2 * Math.sqrt(A) * alpha;
190 b0 = A * (Ap1 - Am1 * k + k2);
191 b1 = 2 * A * (Am1 - Ap1 * k);
192 b2 = A * (Ap1 - Am1 * k - k2);
193 a0 = Ap1 + Am1 * k + k2;
194 a1 = -2 * (Am1 + Ap1 * k);
195 a2 = Ap1 + Am1 * k - k2;
196 coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
202 function createHighShelfFilter(freq, q, gain) {
212 var A = Math.pow(10, gain / 40);
215 // When freq = 1, the z-transform is 1
216 coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
217 } else if (freq > 0) {
218 var w0 = Math.PI * freq;
220 var alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
221 var k = Math.cos(w0);
222 var k2 = 2 * Math.sqrt(A) * alpha;
226 b0 = A * (Ap1 + Am1 * k + k2);
227 b1 = -2 * A * (Am1 + Ap1 * k);
228 b2 = A * (Ap1 + Am1 * k - k2);
229 a0 = Ap1 - Am1 * k + k2;
230 a1 = 2 * (Am1 - Ap1*k);
231 a2 = Ap1 - Am1 * k-k2;
233 coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
235 // When freq = 0, the filter is just a gain
236 coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
242 function createPeakingFilter(freq, q, gain) {
251 var A = Math.pow(10, gain / 40);
253 if (freq > 0 && freq < 1) {
255 var w0 = Math.PI * freq;
256 var alpha = Math.sin(w0) / (2 * q);
257 var k = Math.cos(w0);
266 coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
268 // q = 0, we have a divide by zero problem in the formulas
269 // above. But if we look at the z-transform, we see that the
270 // limit as q approaches 0 is A^2.
271 coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
274 // freq = 0 or 1, the z-transform is 1
275 coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
281 function createNotchFilter(freq, q, gain) {
290 if (freq > 0 && freq < 1) {
292 var w0 = Math.PI * freq;
293 var alpha = Math.sin(w0) / (2 * q);
294 var k = Math.cos(w0);
302 coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
304 // When q = 0, we get a divide by zero above. The limit of the
305 // z-transform as q approaches 0 is 0, so set the coefficients
307 coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
310 // When freq = 0 or 1, the z-transform is 1
311 coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
317 function createAllpassFilter(freq, q, gain) {
326 if (freq > 0 && freq < 1) {
328 var w0 = Math.PI * freq;
329 var alpha = Math.sin(w0) / (2 * q);
330 var k = Math.cos(w0);
338 coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
341 coef = {b0 : -1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
344 coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
350 // Map the filter type name to a function that computes the filter coefficents for the given filter
352 var filterCreatorFunction = {"lowpass": createLowpassFilter,
353 "highpass": createHighpassFilter,
354 "bandpass": createBandpassFilter,
355 "lowshelf": createLowShelfFilter,
356 "highshelf": createHighShelfFilter,
357 "peaking": createPeakingFilter,
358 "notch": createNotchFilter,
359 "allpass": createAllpassFilter};
361 var filterTypeName = {"lowpass": "Lowpass filter",
362 "highpass": "Highpass filter",
363 "bandpass": "Bandpass filter",
364 "lowshelf": "Lowshelf filter",
365 "highshelf": "Highshelf filter",
366 "peaking": "Peaking filter",
367 "notch": "Notch filter",
368 "allpass": "Allpass filter"};
370 function createFilter(filterType, freq, q, gain) {
371 return filterCreatorFunction[filterType](freq, q, gain);
374 function filterData(filterCoef, signal, len) {
375 var y = new Array(len);
376 var b0 = filterCoef.b0;
377 var b1 = filterCoef.b1;
378 var b2 = filterCoef.b2;
379 var a1 = filterCoef.a1;
380 var a2 = filterCoef.a2;
382 // Prime the pump. (Assumes the signal has length >= 2!)
383 y[0] = b0 * signal[0];
384 y[1] = b0 * signal[1] + b1 * signal[0] - a1 * y[0];
386 // Filter all of the signal that we have.
387 for (var k = 2; k < Math.min(signal.length, len); ++k) {
388 y[k] = b0 * signal[k] + b1 * signal[k-1] + b2 * signal[k-2] - a1 * y[k-1] - a2 * y[k-2];
391 // If we need to filter more, but don't have any signal left,
392 // assume the signal is zero.
393 for (var k = signal.length; k < len; ++k) {
394 y[k] = - a1 * y[k-1] - a2 * y[k-2];
400 function createImpulseBuffer(context, length) {
401 var impulse = context.createBuffer(1, length, context.sampleRate);
402 var data = impulse.getChannelData(0);
403 for (var k = 1; k < data.length; ++k) {
412 function createTestAndRun(context, filterType, filterParameters) {
413 // To test the filters, we apply a signal (an impulse) to each of
414 // the specified filters, with each signal starting at a different
415 // time. The output of the filters is summed together at the
416 // output. Thus for filter k, the signal input to the filter
417 // starts at time k * timeStep. For this to work well, timeStep
418 // must be large enough for the output of each filter to have
419 // decayed to zero with timeStep seconds. That way the filter
420 // outputs don't interfere with each other.
422 nFilters = Math.min(filterParameters.length, maxFilters);
424 signal = new Array(nFilters);
425 filter = new Array(nFilters);
427 impulse = createImpulseBuffer(context, pulseLengthFrames);
429 // Create all of the signal sources and filters that we need.
430 for (var k = 0; k < nFilters; ++k) {
431 signal[k] = context.createBufferSource();
432 signal[k].buffer = impulse;
434 filter[k] = context.createBiquadFilter();
435 filter[k].type = filterType;
436 filter[k].frequency.value = context.sampleRate / 2 * filterParameters[k].cutoff;
437 filter[k].detune.value = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
438 filter[k].Q.value = filterParameters[k].q;
439 filter[k].gain.value = filterParameters[k].gain;
441 signal[k].connect(filter[k]);
442 filter[k].connect(context.destination);
444 signal[k].start(timeStep * k);
447 context.oncomplete = checkFilterResponse(filterType, filterParameters);
448 context.startRendering();
451 function addSignal(dest, src, destOffset) {
452 // Add src to dest at the given dest offset.
453 for (var k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
458 function generateReference(filterType, filterParameters) {
459 var result = new Array(renderLengthSamples);
460 var data = new Array(renderLengthSamples);
461 // Initialize the result array and data.
462 for (var k = 0; k < result.length; ++k) {
466 // Make data an impulse.
469 for (var k = 0; k < nFilters; ++k) {
471 var detune = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
472 var frequency = filterParameters[k].cutoff * Math.pow(2, detune / 1200); // Apply detune, converting from Cents.
474 var filterCoef = createFilter(filterType,
476 filterParameters[k].q,
477 filterParameters[k].gain);
478 var y = filterData(filterCoef, data, renderLengthSamples);
480 // Accumulate this filtered data into the final output at the desired offset.
481 addSignal(result, y, timeToSampleFrame(timeStep * k, sampleRate));
487 function checkFilterResponse(filterType, filterParameters) {
488 return function(event) {
489 renderedBuffer = event.renderedBuffer;
490 renderedData = renderedBuffer.getChannelData(0);
492 reference = generateReference(filterType, filterParameters);
494 var len = Math.min(renderedData.length, reference.length);
498 // Maximum error between rendered data and expected data
501 // Sample offset where the maximum error occurred.
504 // Number of infinities or NaNs that occurred in the rendered data.
505 var invalidNumberCount = 0;
507 if (nFilters != filterParameters.length) {
508 testFailed("Test wanted " + filterParameters.length + " filters but only " + maxFilters + " allowed.");
512 // Compare the rendered signal with our reference, keeping
513 // track of the maximum difference (and the offset of the max
514 // difference.) Check for bad numbers in the rendered output
515 // too. There shouldn't be any.
516 for (var k = 0; k < len; ++k) {
517 var err = Math.abs(renderedData[k] - reference[k]);
518 if (err > maxError) {
522 if (!isValidNumber(renderedData[k])) {
523 ++invalidNumberCount;
527 if (invalidNumberCount > 0) {
528 testFailed("Rendered output has " + invalidNumberCount + " infinities or NaNs.");
531 testPassed("Rendered output did not have infinities or NaNs.");
534 if (maxError <= maxAllowedError) {
535 testPassed(filterTypeName[filterType] + " response is correct.");
537 testFailed(filterTypeName[filterType] + " response is incorrect. Max err = " + maxError + " at " + maxPosition + ". Threshold = " + maxAllowedError);
542 testPassed("Test signal was correctly filtered.");
544 testFailed("Test signal was not correctly filtered.");