mirror of
https://github.com/servo/servo.git
synced 2025-09-01 10:38:25 +01:00
Update web-platform-tests to revision 1e4fe87a7f01c0b5c614c8f601ffa68b4a00662a
This commit is contained in:
parent
4c3f1756da
commit
432648745e
164 changed files with 8354 additions and 595 deletions
|
@ -0,0 +1,204 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
Test Basic IIRFilterNode Properties
|
||||
</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="../../resources/audit-util.js"></script>
|
||||
<script src="../../resources/audit.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script id="layout-test-code">
|
||||
let sampleRate = 48000;
|
||||
let testFrames = 100;
|
||||
|
||||
// Global context that can be used by the individual tasks. It must be
|
||||
// defined by the initialize task.
|
||||
let context;
|
||||
|
||||
let audit = Audit.createTaskRunner();
|
||||
|
||||
audit.define('initialize', (task, should) => {
|
||||
should(() => {
|
||||
context = new OfflineAudioContext(1, testFrames, sampleRate);
|
||||
}, 'Initialize context for testing').notThrow();
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.define('existence', (task, should) => {
|
||||
should(context.createIIRFilter, 'context.createIIRFilter').exist();
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.define('parameters', (task, should) => {
|
||||
// Create a really simple IIR filter. Doesn't much matter what.
|
||||
let coef = Float32Array.from([1]);
|
||||
|
||||
let f = context.createIIRFilter(coef, coef);
|
||||
|
||||
should(f.numberOfInputs, 'numberOfInputs').beEqualTo(1);
|
||||
should(f.numberOfOutputs, 'numberOfOutputs').beEqualTo(1);
|
||||
should(f.channelCountMode, 'channelCountMode').beEqualTo('max');
|
||||
should(f.channelInterpretation, 'channelInterpretation')
|
||||
.beEqualTo('speakers');
|
||||
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.define('exceptions-createIIRFilter', (task, should) => {
|
||||
should(function() {
|
||||
// Two args are required.
|
||||
context.createIIRFilter();
|
||||
}, 'createIIRFilter()').throw('TypeError');
|
||||
|
||||
should(function() {
|
||||
// Two args are required.
|
||||
context.createIIRFilter(new Float32Array(1));
|
||||
}, 'createIIRFilter(new Float32Array(1))').throw('TypeError');
|
||||
|
||||
should(function() {
|
||||
// null is not valid
|
||||
context.createIIRFilter(null, null);
|
||||
}, 'createIIRFilter(null, null)').throw('TypeError');
|
||||
|
||||
should(function() {
|
||||
// There has to be at least one coefficient.
|
||||
context.createIIRFilter([], []);
|
||||
}, 'createIIRFilter([], [])').throw('NotSupportedError');
|
||||
|
||||
should(function() {
|
||||
// There has to be at least one coefficient.
|
||||
context.createIIRFilter([1], []);
|
||||
}, 'createIIRFilter([1], [])').throw('NotSupportedError');
|
||||
|
||||
should(function() {
|
||||
// There has to be at least one coefficient.
|
||||
context.createIIRFilter([], [1]);
|
||||
}, 'createIIRFilter([], [1])').throw('NotSupportedError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// Max allowed size for the coefficient arrays.
|
||||
let fb = new Float32Array(20);
|
||||
fb[0] = 1;
|
||||
context.createIIRFilter(fb, fb);
|
||||
},
|
||||
'createIIRFilter(new Float32Array(20), new Float32Array(20))')
|
||||
.notThrow();
|
||||
|
||||
should(
|
||||
function() {
|
||||
// Max allowed size for the feedforward coefficient array.
|
||||
let coef = new Float32Array(21);
|
||||
coef[0] = 1;
|
||||
context.createIIRFilter(coef, [1]);
|
||||
},
|
||||
'createIIRFilter(new Float32Array(21), [1])')
|
||||
.throw('NotSupportedError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// Max allowed size for the feedback coefficient array.
|
||||
let coef = new Float32Array(21);
|
||||
coef[0] = 1;
|
||||
context.createIIRFilter([1], coef);
|
||||
},
|
||||
'createIIRFilter([1], new Float32Array(21))')
|
||||
.throw('NotSupportedError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// First feedback coefficient can't be 0.
|
||||
context.createIIRFilter([1], new Float32Array(2));
|
||||
},
|
||||
'createIIRFilter([1], new Float32Array(2))')
|
||||
.throw('InvalidStateError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// feedforward coefficients can't all be zero.
|
||||
context.createIIRFilter(new Float32Array(10), [1]);
|
||||
},
|
||||
'createIIRFilter(new Float32Array(10), [1])')
|
||||
.throw('InvalidStateError');
|
||||
|
||||
should(function() {
|
||||
// Feedback coefficients must be finite.
|
||||
context.createIIRFilter([1], [1, Infinity, NaN]);
|
||||
}, 'createIIRFilter([1], [1, NaN, Infinity])').throw('TypeError');
|
||||
|
||||
should(function() {
|
||||
// Feedforward coefficients must be finite.
|
||||
context.createIIRFilter([1, Infinity, NaN], [1]);
|
||||
}, 'createIIRFilter([1, NaN, Infinity], [1])').throw('TypeError');
|
||||
|
||||
should(function() {
|
||||
// Test that random junk in the array is converted to NaN.
|
||||
context.createIIRFilter([1, 'abc', []], [1]);
|
||||
}, 'createIIRFilter([1, \'abc\', []], [1])').throw('TypeError');
|
||||
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.define('exceptions-getFrequencyData', (task, should) => {
|
||||
// Create a really simple IIR filter. Doesn't much matter what.
|
||||
let coef = Float32Array.from([1]);
|
||||
|
||||
let f = context.createIIRFilter(coef, coef);
|
||||
|
||||
should(
|
||||
function() {
|
||||
// frequencyHz can't be null.
|
||||
f.getFrequencyResponse(
|
||||
null, new Float32Array(1), new Float32Array(1));
|
||||
},
|
||||
'getFrequencyResponse(null, new Float32Array(1), new Float32Array(1))')
|
||||
.throw('TypeError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// magResponse can't be null.
|
||||
f.getFrequencyResponse(
|
||||
new Float32Array(1), null, new Float32Array(1));
|
||||
},
|
||||
'getFrequencyResponse(new Float32Array(1), null, new Float32Array(1))')
|
||||
.throw('TypeError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// phaseResponse can't be null.
|
||||
f.getFrequencyResponse(
|
||||
new Float32Array(1), new Float32Array(1), null);
|
||||
},
|
||||
'getFrequencyResponse(new Float32Array(1), new Float32Array(1), null)')
|
||||
.throw('TypeError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// magResponse array must the same length as frequencyHz
|
||||
f.getFrequencyResponse(
|
||||
new Float32Array(10), new Float32Array(1),
|
||||
new Float32Array(20));
|
||||
},
|
||||
'getFrequencyResponse(new Float32Array(10), new Float32Array(1), new Float32Array(20))')
|
||||
.throw('InvalidAccessError');
|
||||
|
||||
should(
|
||||
function() {
|
||||
// phaseResponse array must be the same length as frequencyHz
|
||||
f.getFrequencyResponse(
|
||||
new Float32Array(10), new Float32Array(20),
|
||||
new Float32Array(1));
|
||||
},
|
||||
'getFrequencyResponse(new Float32Array(10), new Float32Array(20), new Float32Array(1))')
|
||||
.throw('InvalidAccessError');
|
||||
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,159 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
Test IIRFilter getFrequencyResponse() functionality
|
||||
</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="../../resources/audit-util.js"></script>
|
||||
<script src="../../resources/audit.js"></script>
|
||||
<script src="../../resources/biquad-filters.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script id="layout-test-code">
|
||||
let sampleRate = 48000;
|
||||
// Some short duration; we're not actually looking at the rendered output.
|
||||
let testDurationSec = 0.01;
|
||||
|
||||
// Number of frequency samples to take.
|
||||
let numberOfFrequencies = 1000;
|
||||
|
||||
let audit = Audit.createTaskRunner();
|
||||
|
||||
|
||||
// Compute a set of linearly spaced frequencies.
|
||||
function createFrequencies(nFrequencies, sampleRate) {
|
||||
let frequencies = new Float32Array(nFrequencies);
|
||||
let nyquist = sampleRate / 2;
|
||||
let freqDelta = nyquist / nFrequencies;
|
||||
|
||||
for (let k = 0; k < nFrequencies; ++k) {
|
||||
frequencies[k] = k * freqDelta;
|
||||
}
|
||||
|
||||
return frequencies;
|
||||
}
|
||||
|
||||
audit.define('1-pole IIR', (task, should) => {
|
||||
let context = new OfflineAudioContext(
|
||||
1, testDurationSec * sampleRate, sampleRate);
|
||||
|
||||
let iir = context.createIIRFilter([1], [1, -0.9]);
|
||||
let frequencies =
|
||||
createFrequencies(numberOfFrequencies, context.sampleRate);
|
||||
|
||||
let iirMag = new Float32Array(numberOfFrequencies);
|
||||
let iirPhase = new Float32Array(numberOfFrequencies);
|
||||
let trueMag = new Float32Array(numberOfFrequencies);
|
||||
let truePhase = new Float32Array(numberOfFrequencies);
|
||||
|
||||
// The IIR filter is
|
||||
// H(z) = 1/(1 - 0.9*z^(-1)).
|
||||
//
|
||||
// The frequency response is
|
||||
// H(exp(j*w)) = 1/(1 - 0.9*exp(-j*w)).
|
||||
//
|
||||
// Thus, the magnitude is
|
||||
// |H(exp(j*w))| = 1/sqrt(1.81-1.8*cos(w)).
|
||||
//
|
||||
// The phase is
|
||||
// arg(H(exp(j*w)) = atan(0.9*sin(w)/(.9*cos(w)-1))
|
||||
|
||||
let frequencyScale = Math.PI / (sampleRate / 2);
|
||||
|
||||
for (let k = 0; k < frequencies.length; ++k) {
|
||||
let omega = frequencyScale * frequencies[k];
|
||||
trueMag[k] = 1 / Math.sqrt(1.81 - 1.8 * Math.cos(omega));
|
||||
truePhase[k] =
|
||||
Math.atan(0.9 * Math.sin(omega) / (0.9 * Math.cos(omega) - 1));
|
||||
}
|
||||
|
||||
iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
|
||||
|
||||
// Thresholds were experimentally determined.
|
||||
should(iirMag, '1-pole IIR Magnitude Response')
|
||||
.beCloseToArray(trueMag, {absoluteThreshold: 2.8611e-6});
|
||||
should(iirPhase, '1-pole IIR Phase Response')
|
||||
.beCloseToArray(truePhase, {absoluteThreshold: 1.7882e-7});
|
||||
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.define('compare IIR and biquad', (task, should) => {
|
||||
// Create an IIR filter equivalent to the biquad filter. Compute the
|
||||
// frequency response for both and verify that they are the same.
|
||||
let context = new OfflineAudioContext(
|
||||
1, testDurationSec * sampleRate, sampleRate);
|
||||
|
||||
let biquad = context.createBiquadFilter();
|
||||
let coef = createFilter(
|
||||
biquad.type, biquad.frequency.value / (context.sampleRate / 2),
|
||||
biquad.Q.value, biquad.gain.value);
|
||||
|
||||
let iir = context.createIIRFilter(
|
||||
[coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
|
||||
|
||||
let frequencies =
|
||||
createFrequencies(numberOfFrequencies, context.sampleRate);
|
||||
let biquadMag = new Float32Array(numberOfFrequencies);
|
||||
let biquadPhase = new Float32Array(numberOfFrequencies);
|
||||
let iirMag = new Float32Array(numberOfFrequencies);
|
||||
let iirPhase = new Float32Array(numberOfFrequencies);
|
||||
|
||||
biquad.getFrequencyResponse(frequencies, biquadMag, biquadPhase);
|
||||
iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
|
||||
|
||||
// Thresholds were experimentally determined.
|
||||
should(iirMag, 'IIR Magnitude Response').beCloseToArray(biquadMag, {
|
||||
absoluteThreshold: 2.7419e-5
|
||||
});
|
||||
should(iirPhase, 'IIR Phase Response').beCloseToArray(biquadPhase, {
|
||||
absoluteThreshold: 2.7657e-5
|
||||
});
|
||||
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.define(
|
||||
{
|
||||
label: 'getFrequencyResponse',
|
||||
description: 'Test out-of-bounds frequency values'
|
||||
},
|
||||
(task, should) => {
|
||||
let context = new OfflineAudioContext(1, 1, sampleRate);
|
||||
let filter = new IIRFilterNode(
|
||||
context, {feedforward: [1], feedback: [1, -.9]});
|
||||
|
||||
// Frequencies to test. These are all outside the valid range of
|
||||
// frequencies of 0 to Nyquist.
|
||||
let freq = new Float32Array(2);
|
||||
freq[0] = -1;
|
||||
freq[1] = context.sampleRate / 2 + 1;
|
||||
|
||||
let mag = new Float32Array(freq.length);
|
||||
let phase = new Float32Array(freq.length);
|
||||
|
||||
filter.getFrequencyResponse(freq, mag, phase);
|
||||
|
||||
// Verify that the returned magnitude and phase entries are alL NaN
|
||||
// since the frequencies are outside the valid range
|
||||
for (let k = 0; k < mag.length; ++k) {
|
||||
should(mag[k],
|
||||
'Magnitude response at frequency ' + freq[k])
|
||||
.beNaN();
|
||||
}
|
||||
|
||||
for (let k = 0; k < phase.length; ++k) {
|
||||
should(phase[k],
|
||||
'Phase response at frequency ' + freq[k])
|
||||
.beNaN();
|
||||
}
|
||||
|
||||
task.done();
|
||||
});
|
||||
|
||||
audit.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,572 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
Test Basic IIRFilterNode Operation
|
||||
</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="../../resources/audit-util.js"></script>
|
||||
<script src="../../resources/audit.js"></script>
|
||||
<script src="../../resources/biquad-filters.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script id="layout-test-code">
|
||||
let sampleRate = 24000;
|
||||
let testDurationSec = 0.25;
|
||||
let testFrames = testDurationSec * sampleRate;
|
||||
|
||||
let audit = Audit.createTaskRunner();
|
||||
|
||||
audit.define('coefficient-normalization', (task, should) => {
|
||||
// Test that the feedback coefficients are normalized. Do this be
|
||||
// creating two IIRFilterNodes. One has normalized coefficients, and
|
||||
// one doesn't. Compute the difference and make sure they're the same.
|
||||
let context = new OfflineAudioContext(2, testFrames, sampleRate);
|
||||
|
||||
// Use a simple impulse as the source.
|
||||
let buffer = context.createBuffer(1, 1, sampleRate);
|
||||
buffer.getChannelData(0)[0] = 1;
|
||||
let source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
// Gain node for computing the difference between the filters.
|
||||
let gain = context.createGain();
|
||||
gain.gain.value = -1;
|
||||
|
||||
// The IIR filters. Use a common feedforward array.
|
||||
let ff = [1];
|
||||
|
||||
let fb1 = [1, .9];
|
||||
|
||||
let fb2 = new Float64Array(2);
|
||||
// Scale the feedback coefficients by an arbitrary factor.
|
||||
let coefScaleFactor = 2;
|
||||
for (let k = 0; k < fb2.length; ++k) {
|
||||
fb2[k] = coefScaleFactor * fb1[k];
|
||||
}
|
||||
|
||||
let iir1;
|
||||
let iir2;
|
||||
|
||||
should(function() {
|
||||
iir1 = context.createIIRFilter(ff, fb1);
|
||||
}, 'createIIRFilter with normalized coefficients').notThrow();
|
||||
|
||||
should(function() {
|
||||
iir2 = context.createIIRFilter(ff, fb2);
|
||||
}, 'createIIRFilter with unnormalized coefficients').notThrow();
|
||||
|
||||
// Create the graph. The output of iir1 (normalized coefficients) is
|
||||
// channel 0, and the output of iir2 (unnormalized coefficients), with
|
||||
// appropriate scaling, is channel 1.
|
||||
let merger = context.createChannelMerger(2);
|
||||
source.connect(iir1);
|
||||
source.connect(iir2);
|
||||
iir1.connect(merger, 0, 0);
|
||||
iir2.connect(gain);
|
||||
|
||||
// The gain for the gain node should be set to compensate for the
|
||||
// scaling of the coefficients. Since iir2 has scaled the coefficients
|
||||
// by coefScaleFactor, the output is reduced by the same factor, so
|
||||
// adjust the gain to scale the output of iir2 back up.
|
||||
gain.gain.value = coefScaleFactor;
|
||||
gain.connect(merger, 0, 1);
|
||||
|
||||
merger.connect(context.destination);
|
||||
|
||||
source.start();
|
||||
|
||||
// Rock and roll!
|
||||
|
||||
context.startRendering()
|
||||
.then(function(result) {
|
||||
// Find the max amplitude of the result, which should be near
|
||||
// zero.
|
||||
let iir1Data = result.getChannelData(0);
|
||||
let iir2Data = result.getChannelData(1);
|
||||
|
||||
// Threshold isn't exactly zero because the arithmetic is done
|
||||
// differently between the IIRFilterNode and the BiquadFilterNode.
|
||||
should(
|
||||
iir2Data,
|
||||
'Output of IIR filter with unnormalized coefficients')
|
||||
.beCloseToArray(iir1Data, {absoluteThreshold: 2.1958e-38});
|
||||
})
|
||||
.then(() => task.done());
|
||||
});
|
||||
|
||||
audit.define('one-zero', (task, should) => {
|
||||
// Create a simple 1-zero filter and compare with the expected output.
|
||||
let context = new OfflineAudioContext(1, testFrames, sampleRate);
|
||||
|
||||
// Use a simple impulse as the source
|
||||
let buffer = context.createBuffer(1, 1, sampleRate);
|
||||
buffer.getChannelData(0)[0] = 1;
|
||||
let source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
// The filter is y(n) = 0.5*(x(n) + x(n-1)), a simple 2-point moving
|
||||
// average. This is rather arbitrary; keep it simple.
|
||||
|
||||
let iir = context.createIIRFilter([0.5, 0.5], [1]);
|
||||
|
||||
// Create the graph
|
||||
source.connect(iir);
|
||||
iir.connect(context.destination);
|
||||
|
||||
// Rock and roll!
|
||||
source.start();
|
||||
|
||||
context.startRendering()
|
||||
.then(function(result) {
|
||||
let actual = result.getChannelData(0);
|
||||
let expected = new Float64Array(testFrames);
|
||||
// The filter is a simple 2-point moving average of an impulse, so
|
||||
// the first two values are non-zero and the rest are zero.
|
||||
expected[0] = 0.5;
|
||||
expected[1] = 0.5;
|
||||
should(actual, 'IIR 1-zero output').beCloseToArray(expected, {
|
||||
absoluteThreshold: 0
|
||||
});
|
||||
})
|
||||
.then(() => task.done());
|
||||
});
|
||||
|
||||
audit.define('one-pole', (task, should) => {
|
||||
// Create a simple 1-pole filter and compare with the expected output.
|
||||
|
||||
// The filter is y(n) + c*y(n-1)= x(n). The analytical response is
|
||||
// (-c)^n, so choose a suitable number of frames to run the test for
|
||||
// where the output isn't flushed to zero.
|
||||
let c = 0.9;
|
||||
let eps = 1e-20;
|
||||
let duration = Math.floor(Math.log(eps) / Math.log(Math.abs(c)));
|
||||
let context = new OfflineAudioContext(1, duration, sampleRate);
|
||||
|
||||
// Use a simple impulse as the source
|
||||
let buffer = context.createBuffer(1, 1, sampleRate);
|
||||
buffer.getChannelData(0)[0] = 1;
|
||||
let source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
let iir = context.createIIRFilter([1], [1, c]);
|
||||
|
||||
// Create the graph
|
||||
source.connect(iir);
|
||||
iir.connect(context.destination);
|
||||
|
||||
// Rock and roll!
|
||||
source.start();
|
||||
|
||||
context.startRendering()
|
||||
.then(function(result) {
|
||||
let actual = result.getChannelData(0);
|
||||
let expected = new Float64Array(actual.length);
|
||||
|
||||
// The filter is a simple 1-pole filter: y(n) = -c*y(n-k)+x(n),
|
||||
// with an impulse as the input.
|
||||
expected[0] = 1;
|
||||
for (k = 1; k < testFrames; ++k) {
|
||||
expected[k] = -c * expected[k - 1];
|
||||
}
|
||||
|
||||
// Threshold isn't exactly zero due to round-off in the
|
||||
// single-precision IIRFilterNode computations versus the
|
||||
// double-precision Javascript computations.
|
||||
should(actual, 'IIR 1-pole output').beCloseToArray(expected, {
|
||||
absoluteThreshold: 2.7657e-8
|
||||
});
|
||||
})
|
||||
.then(() => task.done());
|
||||
});
|
||||
|
||||
// Return a function suitable for use as a defineTask function. This
|
||||
// function creates an IIRFilterNode equivalent to the specified
|
||||
// BiquadFilterNode and compares the outputs. The outputs from the two
|
||||
// filters should be virtually identical.
|
||||
function testWithBiquadFilter(filterType, errorThreshold, snrThreshold) {
|
||||
return (task, should) => {
|
||||
let context = new OfflineAudioContext(2, testFrames, sampleRate);
|
||||
|
||||
// Use a constant (step function) as the source
|
||||
let buffer = createConstantBuffer(context, testFrames, 1);
|
||||
let source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
|
||||
// Create the biquad. Choose some rather arbitrary values for Q and
|
||||
// gain for the biquad so that the shelf filters aren't identical.
|
||||
let biquad = context.createBiquadFilter();
|
||||
biquad.type = filterType;
|
||||
biquad.Q.value = 10;
|
||||
biquad.gain.value = 10;
|
||||
|
||||
// Create the equivalent IIR Filter node by computing the coefficients
|
||||
// of the given biquad filter type.
|
||||
let nyquist = sampleRate / 2;
|
||||
let coef = createFilter(
|
||||
filterType, biquad.frequency.value / nyquist, biquad.Q.value,
|
||||
biquad.gain.value);
|
||||
|
||||
let iir = context.createIIRFilter(
|
||||
[coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
|
||||
|
||||
let merger = context.createChannelMerger(2);
|
||||
// Create the graph
|
||||
source.connect(biquad);
|
||||
source.connect(iir);
|
||||
|
||||
biquad.connect(merger, 0, 0);
|
||||
iir.connect(merger, 0, 1);
|
||||
|
||||
merger.connect(context.destination);
|
||||
|
||||
// Rock and roll!
|
||||
source.start();
|
||||
|
||||
context.startRendering()
|
||||
.then(function(result) {
|
||||
// Find the max amplitude of the result, which should be near
|
||||
// zero.
|
||||
let expected = result.getChannelData(0);
|
||||
let actual = result.getChannelData(1);
|
||||
|
||||
// On MacOSX, WebAudio uses an optimized Biquad implementation
|
||||
// that is different from the implementation used for Linux and
|
||||
// Windows. This will cause the output to differ, even if the
|
||||
// threshold passes. Thus, only print out a very small number
|
||||
// of elements of the array where we have tested that they are
|
||||
// consistent.
|
||||
should(actual, 'IIRFilter for Biquad ' + filterType)
|
||||
.beCloseToArray(expected, errorThreshold);
|
||||
|
||||
let snr = 10 * Math.log10(computeSNR(actual, expected));
|
||||
should(snr, 'SNR for IIRFIlter for Biquad ' + filterType)
|
||||
.beGreaterThanOrEqualTo(snrThreshold);
|
||||
})
|
||||
.then(() => task.done());
|
||||
};
|
||||
}
|
||||
|
||||
// Thresholds here are experimentally determined.
|
||||
let biquadTestConfigs = [
|
||||
{
|
||||
filterType: 'lowpass',
|
||||
snrThreshold: 91.221,
|
||||
errorThreshold: {relativeThreshold: 4.9834e-5}
|
||||
},
|
||||
{
|
||||
filterType: 'highpass',
|
||||
snrThreshold: 105.4590,
|
||||
errorThreshold: {absoluteThreshold: 2.9e-6, relativeThreshold: 3e-5}
|
||||
},
|
||||
{
|
||||
filterType: 'bandpass',
|
||||
snrThreshold: 104.060,
|
||||
errorThreshold: {absoluteThreshold: 2e-7, relativeThreshold: 8.7e-4}
|
||||
},
|
||||
{
|
||||
filterType: 'notch',
|
||||
snrThreshold: 91.312,
|
||||
errorThreshold: {absoluteThreshold: 0, relativeThreshold: 4.22e-5}
|
||||
},
|
||||
{
|
||||
filterType: 'allpass',
|
||||
snrThreshold: 91.319,
|
||||
errorThreshold: {absoluteThreshold: 0, relativeThreshold: 4.31e-5}
|
||||
},
|
||||
{
|
||||
filterType: 'lowshelf',
|
||||
snrThreshold: 90.609,
|
||||
errorThreshold: {absoluteThreshold: 0, relativeThreshold: 2.98e-5}
|
||||
},
|
||||
{
|
||||
filterType: 'highshelf',
|
||||
snrThreshold: 103.159,
|
||||
errorThreshold: {absoluteThreshold: 0, relativeThreshold: 1.24e-5}
|
||||
},
|
||||
{
|
||||
filterType: 'peaking',
|
||||
snrThreshold: 91.504,
|
||||
errorThreshold: {absoluteThreshold: 0, relativeThreshold: 5.05e-5}
|
||||
}
|
||||
];
|
||||
|
||||
// Create a set of tasks based on biquadTestConfigs.
|
||||
for (k = 0; k < biquadTestConfigs.length; ++k) {
|
||||
let config = biquadTestConfigs[k];
|
||||
let name = k + ': ' + config.filterType;
|
||||
audit.define(
|
||||
name,
|
||||
testWithBiquadFilter(
|
||||
config.filterType, config.errorThreshold, config.snrThreshold));
|
||||
}
|
||||
|
||||
audit.define('multi-channel', (task, should) => {
|
||||
// Multi-channel test. Create a biquad filter and the equivalent IIR
|
||||
// filter. Filter the same multichannel signal and compare the results.
|
||||
let nChannels = 3;
|
||||
let context =
|
||||
new OfflineAudioContext(nChannels, testFrames, sampleRate);
|
||||
|
||||
// Create a set of oscillators as the multi-channel source.
|
||||
let source = [];
|
||||
|
||||
for (k = 0; k < nChannels; ++k) {
|
||||
source[k] = context.createOscillator();
|
||||
source[k].type = 'sawtooth';
|
||||
// The frequency of the oscillator is pretty arbitrary, but each
|
||||
// oscillator should have a different frequency.
|
||||
source[k].frequency.value = 100 + k * 100;
|
||||
}
|
||||
|
||||
let merger = context.createChannelMerger(3);
|
||||
|
||||
let biquad = context.createBiquadFilter();
|
||||
|
||||
// Create the equivalent IIR Filter node.
|
||||
let nyquist = sampleRate / 2;
|
||||
let coef = createFilter(
|
||||
biquad.type, biquad.frequency.value / nyquist, biquad.Q.value,
|
||||
biquad.gain.value);
|
||||
let fb = [1, coef.a1, coef.a2];
|
||||
let ff = [coef.b0, coef.b1, coef.b2];
|
||||
|
||||
let iir = context.createIIRFilter(ff, fb);
|
||||
// Gain node to compute the difference between the IIR and biquad
|
||||
// filter.
|
||||
let gain = context.createGain();
|
||||
gain.gain.value = -1;
|
||||
|
||||
// Create the graph.
|
||||
for (k = 0; k < nChannels; ++k)
|
||||
source[k].connect(merger, 0, k);
|
||||
|
||||
merger.connect(biquad);
|
||||
merger.connect(iir);
|
||||
iir.connect(gain);
|
||||
biquad.connect(context.destination);
|
||||
gain.connect(context.destination);
|
||||
|
||||
for (k = 0; k < nChannels; ++k)
|
||||
source[k].start();
|
||||
|
||||
context.startRendering()
|
||||
.then(function(result) {
|
||||
let errorThresholds = [3.7671e-5, 3.0071e-5, 2.6241e-5];
|
||||
|
||||
// Check the difference signal on each channel
|
||||
for (channel = 0; channel < result.numberOfChannels; ++channel) {
|
||||
// Find the max amplitude of the result, which should be near
|
||||
// zero.
|
||||
let data = result.getChannelData(channel);
|
||||
let maxError =
|
||||
data.reduce(function(reducedValue, currentValue) {
|
||||
return Math.max(reducedValue, Math.abs(currentValue));
|
||||
});
|
||||
|
||||
should(
|
||||
maxError,
|
||||
'Max difference between IIR and Biquad on channel ' +
|
||||
channel)
|
||||
.beLessThanOrEqualTo(errorThresholds[channel]);
|
||||
}
|
||||
|
||||
})
|
||||
.then(() => task.done());
|
||||
});
|
||||
|
||||
// Apply an IIRFilter to the given input signal.
|
||||
//
|
||||
// IIR filter in the time domain is
|
||||
//
|
||||
// y[n] = sum(ff[k]*x[n-k], k, 0, M) - sum(fb[k]*y[n-k], k, 1, N)
|
||||
//
|
||||
function iirFilter(input, feedforward, feedback) {
|
||||
// For simplicity, create an x buffer that contains the input, and a y
|
||||
// buffer that contains the output. Both of these buffers have an
|
||||
// initial work space to implement the initial memory of the filter.
|
||||
let workSize = Math.max(feedforward.length, feedback.length);
|
||||
let x = new Float32Array(input.length + workSize);
|
||||
|
||||
// Float64 because we want to match the implementation that uses doubles
|
||||
// to minimize roundoff.
|
||||
let y = new Float64Array(input.length + workSize);
|
||||
|
||||
// Copy the input over.
|
||||
for (let k = 0; k < input.length; ++k)
|
||||
x[k + feedforward.length] = input[k];
|
||||
|
||||
// Run the filter
|
||||
for (let n = 0; n < input.length; ++n) {
|
||||
let index = n + workSize;
|
||||
let yn = 0;
|
||||
for (let k = 0; k < feedforward.length; ++k)
|
||||
yn += feedforward[k] * x[index - k];
|
||||
for (let k = 0; k < feedback.length; ++k)
|
||||
yn -= feedback[k] * y[index - k];
|
||||
|
||||
y[index] = yn;
|
||||
}
|
||||
|
||||
return y.slice(workSize).map(Math.fround);
|
||||
}
|
||||
|
||||
// Cascade the two given biquad filters to create one IIR filter.
|
||||
function cascadeBiquads(f1Coef, f2Coef) {
|
||||
// The biquad filters are:
|
||||
//
|
||||
// f1 = (b10 + b11/z + b12/z^2)/(1 + a11/z + a12/z^2);
|
||||
// f2 = (b20 + b21/z + b22/z^2)/(1 + a21/z + a22/z^2);
|
||||
//
|
||||
// To cascade them, multiply the two transforms together to get a fourth
|
||||
// order IIR filter.
|
||||
|
||||
let numProduct = [
|
||||
f1Coef.b0 * f2Coef.b0, f1Coef.b0 * f2Coef.b1 + f1Coef.b1 * f2Coef.b0,
|
||||
f1Coef.b0 * f2Coef.b2 + f1Coef.b1 * f2Coef.b1 + f1Coef.b2 * f2Coef.b0,
|
||||
f1Coef.b1 * f2Coef.b2 + f1Coef.b2 * f2Coef.b1, f1Coef.b2 * f2Coef.b2
|
||||
];
|
||||
|
||||
let denProduct = [
|
||||
1, f2Coef.a1 + f1Coef.a1,
|
||||
f2Coef.a2 + f1Coef.a1 * f2Coef.a1 + f1Coef.a2,
|
||||
f1Coef.a1 * f2Coef.a2 + f1Coef.a2 * f2Coef.a1, f1Coef.a2 * f2Coef.a2
|
||||
];
|
||||
|
||||
return {
|
||||
ff: numProduct, fb: denProduct
|
||||
}
|
||||
}
|
||||
|
||||
// Find the magnitude of the root of the quadratic that has the maximum
|
||||
// magnitude.
|
||||
//
|
||||
// The quadratic is z^2 + a1 * z + a2 and we want the root z that has the
|
||||
// largest magnitude.
|
||||
function largestRootMagnitude(a1, a2) {
|
||||
let discriminant = a1 * a1 - 4 * a2;
|
||||
if (discriminant < 0) {
|
||||
// Complex roots: -a1/2 +/- i*sqrt(-d)/2. Thus the magnitude of each
|
||||
// root is the same and is sqrt(a1^2/4 + |d|/4)
|
||||
let d = Math.sqrt(-discriminant);
|
||||
return Math.hypot(a1 / 2, d / 2);
|
||||
} else {
|
||||
// Real roots
|
||||
let d = Math.sqrt(discriminant);
|
||||
return Math.max(Math.abs((-a1 + d) / 2), Math.abs((-a1 - d) / 2));
|
||||
}
|
||||
}
|
||||
|
||||
audit.define('4th-order-iir', (task, should) => {
|
||||
// Cascade 2 lowpass biquad filters and compare that with the equivalent
|
||||
// 4th order IIR filter.
|
||||
|
||||
let nyquist = sampleRate / 2;
|
||||
// Compute the coefficients of a lowpass filter.
|
||||
|
||||
// First some preliminary stuff. Compute the coefficients of the
|
||||
// biquad. This is used to figure out how frames to use in the test.
|
||||
let biquadType = 'lowpass';
|
||||
let biquadCutoff = 350;
|
||||
let biquadQ = 5;
|
||||
let biquadGain = 1;
|
||||
|
||||
let coef = createFilter(
|
||||
biquadType, biquadCutoff / nyquist, biquadQ, biquadGain);
|
||||
|
||||
// Cascade the biquads together to create an equivalent IIR filter.
|
||||
let cascade = cascadeBiquads(coef, coef);
|
||||
|
||||
// Since we're cascading two identical biquads, the root of denominator
|
||||
// of the IIR filter is repeated, so the root of the denominator with
|
||||
// the largest magnitude occurs twice. The impulse response of the IIR
|
||||
// filter will be roughly c*(r*r)^n at time n, where r is the root of
|
||||
// largest magnitude. This approximation gets better as n increases.
|
||||
// We can use this to get a rough idea of when the response has died
|
||||
// down to a small value.
|
||||
|
||||
// This is the value we will use to determine how many frames to render.
|
||||
// Rendering too many is a waste of time and also makes it hard to
|
||||
// compare the actual result to the expected because the magnitudes are
|
||||
// so small that they could be mostly round-off noise.
|
||||
//
|
||||
// Find magnitude of the root with largest magnitude
|
||||
let rootMagnitude = largestRootMagnitude(coef.a1, coef.a2);
|
||||
|
||||
// Find n such that |r|^(2*n) <= eps. That is, n = log(eps)/(2*log(r)).
|
||||
// Somewhat arbitrarily choose eps = 1e-20;
|
||||
let eps = 1e-20;
|
||||
let framesForTest =
|
||||
Math.floor(Math.log(eps) / (2 * Math.log(rootMagnitude)));
|
||||
|
||||
// We're ready to create the graph for the test. The offline context
|
||||
// has two channels: channel 0 is the expected (cascaded biquad) result
|
||||
// and channel 1 is the actual IIR filter result.
|
||||
let context = new OfflineAudioContext(2, framesForTest, sampleRate);
|
||||
|
||||
// Use a simple impulse with a large (arbitrary) amplitude as the source
|
||||
let amplitude = 1;
|
||||
let buffer = context.createBuffer(1, testFrames, sampleRate);
|
||||
buffer.getChannelData(0)[0] = amplitude;
|
||||
let source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
// Create the two biquad filters. Doesn't really matter what, but for
|
||||
// simplicity we choose identical lowpass filters with the same
|
||||
// parameters.
|
||||
let biquad1 = context.createBiquadFilter();
|
||||
biquad1.type = biquadType;
|
||||
biquad1.frequency.value = biquadCutoff;
|
||||
biquad1.Q.value = biquadQ;
|
||||
|
||||
let biquad2 = context.createBiquadFilter();
|
||||
biquad2.type = biquadType;
|
||||
biquad2.frequency.value = biquadCutoff;
|
||||
biquad2.Q.value = biquadQ;
|
||||
|
||||
let iir = context.createIIRFilter(cascade.ff, cascade.fb);
|
||||
|
||||
// Create the merger to get the signals into multiple channels
|
||||
let merger = context.createChannelMerger(2);
|
||||
|
||||
// Create the graph, filtering the source through two biquads.
|
||||
source.connect(biquad1);
|
||||
biquad1.connect(biquad2);
|
||||
biquad2.connect(merger, 0, 0);
|
||||
|
||||
source.connect(iir);
|
||||
iir.connect(merger, 0, 1);
|
||||
|
||||
merger.connect(context.destination);
|
||||
|
||||
// Now filter the source through the IIR filter.
|
||||
let y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);
|
||||
|
||||
// Rock and roll!
|
||||
source.start();
|
||||
|
||||
context.startRendering()
|
||||
.then(function(result) {
|
||||
let expected = result.getChannelData(0);
|
||||
let actual = result.getChannelData(1);
|
||||
|
||||
should(actual, '4-th order IIRFilter (biquad ref)')
|
||||
.beCloseToArray(expected, {
|
||||
// Thresholds experimentally determined.
|
||||
absoluteThreshold: 1.59e-7,
|
||||
relativeThreshold: 2.11e-5,
|
||||
});
|
||||
|
||||
let snr = 10 * Math.log10(computeSNR(actual, expected));
|
||||
should(snr, 'SNR of 4-th order IIRFilter (biquad ref)')
|
||||
.beGreaterThanOrEqualTo(108.947);
|
||||
})
|
||||
.then(() => task.done());
|
||||
});
|
||||
|
||||
audit.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue