mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-25 11:15:34 +00:00
Bug 1265395
- Import blink biquad filter tests; r=padenot
Imported from git revision b1fd9cf76c2e80540100262b09911600936f2ae3. MozReview-Commit-ID: HzfHxOExJxq --HG-- extra : rebase_source : 977261aefc956d0f6e733eacc5c7c17a2bb29236
This commit is contained in:
parent
0dc0ba53a2
commit
ee7445c176
45
dom/media/webaudio/test/blink/biquad-allpass.html
Normal file
45
dom/media/webaudio/test/blink/biquad-allpass.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad allpass filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
var filterParameters = [{cutoff : 0, q : 10, gain : 1 },
|
||||
{cutoff : 1, q : 10, gain : 1 },
|
||||
{cutoff : .5, q : 0, gain : 1 },
|
||||
{cutoff : 0.25, q : 10, gain : 1 },
|
||||
];
|
||||
createTestAndRun(context, "allpass", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
356
dom/media/webaudio/test/blink/biquad-automation.html
Normal file
356
dom/media/webaudio/test/blink/biquad-automation.html
Normal file
@ -0,0 +1,356 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Biquad Automation Test</title>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/audioparam-testing.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
description("Test Automation of Biquad Filters");
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Don't need to run these tests at high sampling rate, so just use a low one to reduce memory
|
||||
// usage and complexity.
|
||||
var sampleRate = 16000;
|
||||
|
||||
// How long to render for each test.
|
||||
var renderDuration = 1;
|
||||
|
||||
var audit = Audit.createTaskRunner();
|
||||
|
||||
// The definition of the linear ramp automation function.
|
||||
function linearRamp(t, v0, v1, t0, t1) {
|
||||
return v0 + (v1 - v0) * (t - t0) / (t1 - t0);
|
||||
}
|
||||
|
||||
// Generate the filter coefficients for the specified filter using the given parameters for
|
||||
// the given duration. |filterTypeFunction| is a function that returns the filter
|
||||
// coefficients for one set of parameters. |parameters| is a property bag that contains the
|
||||
// start and end values (as an array) for each of the biquad attributes. The properties are
|
||||
// |freq|, |Q|, |gain|, and |detune|. |duration| is the number of seconds for which the
|
||||
// coefficients are generated.
|
||||
//
|
||||
// A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|. Each propery is an array
|
||||
// consisting of the coefficients for the time-varying biquad filter.
|
||||
function generateFilterCoefficients(filterTypeFunction, parameters, duration) {
|
||||
var endFrame = Math.ceil(duration * sampleRate);
|
||||
var nCoef = endFrame;
|
||||
var b0 = new Float64Array(nCoef);
|
||||
var b1 = new Float64Array(nCoef);
|
||||
var b2 = new Float64Array(nCoef);
|
||||
var a1 = new Float64Array(nCoef);
|
||||
var a2 = new Float64Array(nCoef);
|
||||
|
||||
var k = 0;
|
||||
// If the property is not given, use the defaults.
|
||||
var freqs = parameters.freq || [350, 350];
|
||||
var qs = parameters.Q || [1, 1];
|
||||
var gains = parameters.gain || [0, 0];
|
||||
var detunes = parameters.detune || [0, 0];
|
||||
|
||||
for (var frame = 0; frame < endFrame; ++frame) {
|
||||
// Apply linear ramp at frame |frame|.
|
||||
var f = linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, duration);
|
||||
var q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration);
|
||||
var g = linearRamp(frame / sampleRate, gains[0], gains[1], 0, duration);
|
||||
var d = linearRamp(frame / sampleRate, detunes[0], detunes[1], 0, duration);
|
||||
|
||||
// Compute actual frequency parameter
|
||||
f = f * Math.pow(2, d / 1200);
|
||||
|
||||
// Compute filter coefficients
|
||||
var coef = filterTypeFunction(f / (sampleRate / 2), q, g);
|
||||
b0[k] = coef.b0;
|
||||
b1[k] = coef.b1;
|
||||
b2[k] = coef.b2;
|
||||
a1[k] = coef.a1;
|
||||
a2[k] = coef.a2;
|
||||
++k;
|
||||
}
|
||||
|
||||
return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2};
|
||||
}
|
||||
|
||||
// Apply the given time-varying biquad filter to the given signal, |signal|. |coef| should be
|
||||
// the time-varying coefficients of the filter, as returned by |generateFilterCoefficients|.
|
||||
function timeVaryingFilter(signal, coef) {
|
||||
var length = signal.length;
|
||||
// Use double precision for the internal computations.
|
||||
var y = new Float64Array(length);
|
||||
|
||||
// Prime the pump. (Assumes the signal has length >= 2!)
|
||||
y[0] = coef.b0[0] * signal[0];
|
||||
y[1] = coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[0];
|
||||
|
||||
for (var n = 2; n < length; ++n) {
|
||||
y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n-1] + coef.b2[n] * signal[n-2];
|
||||
y[n] -= coef.a1[n] * y[n-1] + coef.a2[n] * y[n-2];
|
||||
}
|
||||
|
||||
// But convert the result to single precision for comparison.
|
||||
return y.map(Math.fround);
|
||||
}
|
||||
|
||||
// Configure the audio graph using |context|. Returns the biquad filter node and the
|
||||
// AudioBuffer used for the source.
|
||||
function configureGraph(context, toneFrequency) {
|
||||
// The source is just a simple sine wave.
|
||||
var src = context.createBufferSource();
|
||||
var b = context.createBuffer(1, renderDuration * sampleRate, sampleRate);
|
||||
var data = b.getChannelData(0);
|
||||
var omega = 2 * Math.PI * toneFrequency / sampleRate;
|
||||
for (var k = 0; k < data.length; ++k) {
|
||||
data[k] = Math.sin(omega * k);
|
||||
}
|
||||
src.buffer = b;
|
||||
var f = context.createBiquadFilter();
|
||||
src.connect(f);
|
||||
f.connect(context.destination);
|
||||
|
||||
src.start();
|
||||
|
||||
return {filter: f, source: b};
|
||||
}
|
||||
|
||||
function createFilterVerifier(filterCreator, threshold, parameters, input, message) {
|
||||
return function (resultBuffer) {
|
||||
var actual = resultBuffer.getChannelData(0);
|
||||
var coefs = generateFilterCoefficients(filterCreator, parameters, renderDuration);
|
||||
|
||||
reference = timeVaryingFilter(input, coefs);
|
||||
|
||||
Should(message, actual).beCloseToArray(reference, threshold);
|
||||
};
|
||||
}
|
||||
|
||||
// Automate just the frequency parameter. A bandpass filter is used where the center
|
||||
// frequency is swept across the source (which is a simple tone).
|
||||
audit.defineTask("automate-freq", function (done) {
|
||||
var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
|
||||
|
||||
// Center frequency of bandpass filter and also the frequency of the test tone.
|
||||
var centerFreq = 10*440;
|
||||
|
||||
// Sweep the frequency +/- 9*440 Hz from the center. This should cause the output to low at
|
||||
// the beginning and end of the test where the done is outside the pass band of the filter,
|
||||
// but high in the center where the tone is near the center of the pass band.
|
||||
var parameters = {
|
||||
freq: [centerFreq - 9*440, centerFreq + 9*440]
|
||||
}
|
||||
var graph = configureGraph(context, centerFreq);
|
||||
var f = graph.filter;
|
||||
var b = graph.source;
|
||||
|
||||
f.type = "bandpass";
|
||||
f.frequency.setValueAtTime(parameters.freq[0], 0);
|
||||
f.frequency.linearRampToValueAtTime(parameters.freq[1], renderDuration);
|
||||
|
||||
context.startRendering()
|
||||
.then(createFilterVerifier(createBandpassFilter, 5e-5, parameters, b.getChannelData(0),
|
||||
"Output of bandpass filter with frequency automation"))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
// Automate just the Q parameter. A bandpass filter is used where the Q of the filter is
|
||||
// swept.
|
||||
audit.defineTask("automate-q", function (done) {
|
||||
var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
|
||||
|
||||
// The frequency of the test tone.
|
||||
var centerFreq = 440;
|
||||
|
||||
// Sweep the Q paramter between 1 and 200. This will cause the output of the filter to pass
|
||||
// most of the tone at the beginning to passing less of the tone at the end. This is
|
||||
// because we set center frequency of the bandpass filter to be slightly off from the actual
|
||||
// tone.
|
||||
var parameters = {
|
||||
Q: [1, 200],
|
||||
// Center frequency of the bandpass filter is just 25 Hz above the tone frequency.
|
||||
freq: [centerFreq + 25, centerFreq + 25]
|
||||
};
|
||||
var graph = configureGraph(context, centerFreq);
|
||||
var f = graph.filter;
|
||||
var b = graph.source;
|
||||
|
||||
f.type = "bandpass";
|
||||
f.frequency.value = parameters.freq[0];
|
||||
f.Q.setValueAtTime(parameters.Q[0], 0);
|
||||
f.Q.linearRampToValueAtTime(parameters.Q[1], renderDuration);
|
||||
|
||||
context.startRendering()
|
||||
.then(createFilterVerifier(createBandpassFilter, 1.4e-6, parameters, b.getChannelData(0),
|
||||
"Output of bandpass filter with Q automation"))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
// Automate just the gain of the lowshelf filter. A test tone will be in the lowshelf part of
|
||||
// the filter. The output will vary as the gain of the lowshelf is changed.
|
||||
audit.defineTask("automate-gain", function (done) {
|
||||
var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
|
||||
|
||||
// Frequency of the test tone.
|
||||
var centerFreq = 440;
|
||||
|
||||
// Set the cutoff frequency of the lowshelf to be significantly higher than the test tone.
|
||||
// Sweep the gain from 20 dB to -20 dB. (We go from 20 to -20 to easily verify that the
|
||||
// filter didn't go unstable.)
|
||||
var parameters = {
|
||||
freq: [3500, 3500],
|
||||
gain: [20, -20]
|
||||
}
|
||||
var graph = configureGraph(context, centerFreq);
|
||||
var f = graph.filter;
|
||||
var b = graph.source;
|
||||
|
||||
f.type = "lowshelf";
|
||||
f.frequency.value = parameters.freq[0];
|
||||
f.gain.setValueAtTime(parameters.gain[0], 0);
|
||||
f.gain.linearRampToValueAtTime(parameters.gain[1], renderDuration);
|
||||
|
||||
context.startRendering()
|
||||
.then(createFilterVerifier(createLowShelfFilter, 8e-6, parameters, b.getChannelData(0),
|
||||
"Output of lowshelf filter with gain automation"))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
// Automate just the detune parameter. Basically the same test as for the frequncy parameter
|
||||
// but we just use the detune parameter to modulate the frequency parameter.
|
||||
audit.defineTask("automate-detune", function (done) {
|
||||
var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
|
||||
var centerFreq = 10*440;
|
||||
var parameters = {
|
||||
freq: [centerFreq, centerFreq],
|
||||
detune: [-10*1200, 10*1200]
|
||||
};
|
||||
var graph = configureGraph(context, centerFreq);
|
||||
var f = graph.filter;
|
||||
var b = graph.source;
|
||||
|
||||
f.type = "bandpass";
|
||||
f.frequency.value = parameters.freq[0];
|
||||
f.detune.setValueAtTime(parameters.detune[0], 0);
|
||||
f.detune.linearRampToValueAtTime(parameters.detune[1], renderDuration);
|
||||
|
||||
context.startRendering()
|
||||
.then(createFilterVerifier(createBandpassFilter, 5e-6, parameters, b.getChannelData(0),
|
||||
"Output of bandpass filter with detune automation"))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
// Automate all of the filter parameters at once. This is a basic check that everything is
|
||||
// working. A peaking filter is used because it uses all of the parameters.
|
||||
audit.defineTask("automate-all", function (done) {
|
||||
var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
|
||||
var graph = configureGraph(context, 10*440);
|
||||
var f = graph.filter;
|
||||
var b = graph.source;
|
||||
|
||||
// Sweep all of the filter parameters. These are pretty much arbitrary.
|
||||
var parameters = {
|
||||
freq: [10000, 100],
|
||||
Q: [f.Q.value, .0001],
|
||||
gain: [f.gain.value, 20],
|
||||
detune: [2400, -2400]
|
||||
};
|
||||
|
||||
f.type = "peaking";
|
||||
// Set starting points for all parameters of the filter. Start at 10 kHz for the center
|
||||
// frequency, and the defaults for Q and gain.
|
||||
f.frequency.setValueAtTime(parameters.freq[0], 0);
|
||||
f.Q.setValueAtTime(parameters.Q[0], 0);
|
||||
f.gain.setValueAtTime(parameters.gain[0], 0);
|
||||
f.detune.setValueAtTime(parameters.detune[0], 0);
|
||||
|
||||
// Linear ramp each parameter
|
||||
f.frequency.linearRampToValueAtTime(parameters.freq[1], renderDuration);
|
||||
f.Q.linearRampToValueAtTime(parameters.Q[1], renderDuration);
|
||||
f.gain.linearRampToValueAtTime(parameters.gain[1], renderDuration);
|
||||
f.detune.linearRampToValueAtTime(parameters.detune[1], renderDuration);
|
||||
|
||||
context.startRendering()
|
||||
.then(createFilterVerifier(createPeakingFilter, 3.3e-4, parameters, b.getChannelData(0),
|
||||
"Output of peaking filter with automation of all parameters"))
|
||||
.then(done);
|
||||
});
|
||||
|
||||
// Test that modulation of the frequency parameter of the filter works. A sinusoid of 440 Hz
|
||||
// is the test signal that is applied to a bandpass biquad filter. The frequency parameter of
|
||||
// the filter is modulated by a sinusoid at 103 Hz, and the frequency modulation varies from
|
||||
// 116 to 412 Hz. (This test was taken from the description in
|
||||
// https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731355)
|
||||
audit.defineTask("modulation", function (done) {
|
||||
var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
|
||||
|
||||
// Create a graph with the sinusoidal source at 440 Hz as the input to a biquad filter.
|
||||
var graph = configureGraph(context, 440);
|
||||
var f = graph.filter;
|
||||
var b = graph.source;
|
||||
|
||||
f.type = "bandpass";
|
||||
f.Q.value = 5;
|
||||
f.frequency.value = 264;
|
||||
|
||||
// Create the modulation source, a sinusoid with frequency 103 Hz and amplitude 148. (The
|
||||
// amplitude of 148 is added to the filter's frequency value of 264 to produce a sinusoidal
|
||||
// modulation of the frequency parameter from 116 to 412 Hz.)
|
||||
var mod = context.createBufferSource();
|
||||
var mbuffer = context.createBuffer(1, renderDuration * sampleRate, sampleRate);
|
||||
var d = mbuffer.getChannelData(0);
|
||||
var omega = 2 * Math.PI * 103 / sampleRate;
|
||||
for (var k = 0; k < d.length; ++k) {
|
||||
d[k] = 148 * Math.sin(omega * k);
|
||||
}
|
||||
mod.buffer = mbuffer;
|
||||
|
||||
mod.connect(f.frequency);
|
||||
|
||||
mod.start();
|
||||
context.startRendering()
|
||||
.then(function (resultBuffer) {
|
||||
var actual = resultBuffer.getChannelData(0);
|
||||
// Compute the filter coefficients using the mod sine wave
|
||||
|
||||
var endFrame = Math.ceil(renderDuration * sampleRate);
|
||||
var nCoef = endFrame;
|
||||
var b0 = new Float64Array(nCoef);
|
||||
var b1 = new Float64Array(nCoef);
|
||||
var b2 = new Float64Array(nCoef);
|
||||
var a1 = new Float64Array(nCoef);
|
||||
var a2 = new Float64Array(nCoef);
|
||||
|
||||
// Generate the filter coefficients when the frequency varies from 116 to 248 Hz using
|
||||
// the 103 Hz sinusoid.
|
||||
for (var k = 0; k < nCoef; ++k) {
|
||||
var freq = f.frequency.value + d[k];
|
||||
var c = createBandpassFilter(freq / (sampleRate / 2), f.Q.value, f.gain.value);
|
||||
b0[k] = c.b0;
|
||||
b1[k] = c.b1;
|
||||
b2[k] = c.b2;
|
||||
a1[k] = c.a1;
|
||||
a2[k] = c.a2;
|
||||
}
|
||||
reference = timeVaryingFilter(b.getChannelData(0),
|
||||
{b0: b0, b1: b1, b2: b2, a1: a1, a2: a2});
|
||||
|
||||
Should("Output of bandpass filter with sinusoidal modulation of bandpass center frequency",
|
||||
actual)
|
||||
.beCloseToArray(reference, 4e-6);
|
||||
})
|
||||
.then(done);
|
||||
});
|
||||
|
||||
// All done!
|
||||
audit.defineTask("finish", function (done) {
|
||||
finishJSTest();
|
||||
done();
|
||||
});
|
||||
|
||||
audit.runTasks();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
47
dom/media/webaudio/test/blink/biquad-bandpass.html
Normal file
47
dom/media/webaudio/test/blink/biquad-bandpass.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad bandpass filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
// The filters we want to test.
|
||||
var filterParameters = [{cutoff : 0, q : 0, gain : 1 },
|
||||
{cutoff : 1, q : 0, gain : 1 },
|
||||
{cutoff : 0.5, q : 0, gain : 1 },
|
||||
{cutoff : 0.25, q : 1, gain : 1 },
|
||||
];
|
||||
|
||||
createTestAndRun(context, "bandpass", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
315
dom/media/webaudio/test/blink/biquad-getFrequencyResponse.html
Normal file
315
dom/media/webaudio/test/blink/biquad-getFrequencyResponse.html
Normal file
@ -0,0 +1,315 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Test Biquad getFrequencyResponse() functionality.");
|
||||
|
||||
// Test the frequency response of a biquad filter. We compute the frequency response for a simple
|
||||
// peaking biquad filter and compare it with the expected frequency response. The actual filter
|
||||
// used doesn't matter since we're testing getFrequencyResponse and not the actual filter output.
|
||||
// The filters are extensively tested in other biquad tests.
|
||||
|
||||
var context;
|
||||
|
||||
// The biquad filter node.
|
||||
var filter;
|
||||
|
||||
// The magnitude response of the biquad filter.
|
||||
var magResponse;
|
||||
|
||||
// The phase response of the biquad filter.
|
||||
var phaseResponse;
|
||||
|
||||
// Number of frequency samples to take.
|
||||
var numberOfFrequencies = 1000;
|
||||
|
||||
// The filter parameters.
|
||||
var filterCutoff = 1000; // Hz.
|
||||
var filterQ = 1;
|
||||
var filterGain = 5; // Decibels.
|
||||
|
||||
// The maximum allowed error in the magnitude response.
|
||||
var maxAllowedMagError = 5.7e-7;
|
||||
|
||||
// The maximum allowed error in the phase response.
|
||||
var maxAllowedPhaseError = 4.7e-8;
|
||||
|
||||
// The magnitudes and phases of the reference frequency response.
|
||||
var magResponse;
|
||||
var phaseResponse;
|
||||
|
||||
// The magnitudes and phases of the reference frequency response.
|
||||
var expectedMagnitudes;
|
||||
var expectedPhases;
|
||||
|
||||
// Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the
|
||||
// Nyquist frequency.
|
||||
function normalizedFrequency(freqHz, sampleRate)
|
||||
{
|
||||
var nyquist = sampleRate / 2;
|
||||
return freqHz / nyquist;
|
||||
}
|
||||
|
||||
// Get the filter response at a (normalized) frequency |f| for the filter with coefficients |coef|.
|
||||
function getResponseAt(coef, f)
|
||||
{
|
||||
var b0 = coef.b0;
|
||||
var b1 = coef.b1;
|
||||
var b2 = coef.b2;
|
||||
var a1 = coef.a1;
|
||||
var a2 = coef.a2;
|
||||
|
||||
// H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
|
||||
//
|
||||
// Compute H(exp(i * pi * f)). No native complex numbers in javascript, so break H(exp(i * pi * // f))
|
||||
// in to the real and imaginary parts of the numerator and denominator. Let omega = pi * f.
|
||||
// Then the numerator is
|
||||
//
|
||||
// b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) + b2 * sin(2 * omega))
|
||||
//
|
||||
// and the denominator is
|
||||
//
|
||||
// 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2 * sin(2 * omega))
|
||||
//
|
||||
// Compute the magnitude and phase from the real and imaginary parts.
|
||||
|
||||
var omega = Math.PI * f;
|
||||
var numeratorReal = b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
|
||||
var numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
|
||||
var denominatorReal = 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
|
||||
var denominatorImag = -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));
|
||||
|
||||
var magnitude = Math.sqrt((numeratorReal * numeratorReal + numeratorImag * numeratorImag)
|
||||
/ (denominatorReal * denominatorReal + denominatorImag * denominatorImag));
|
||||
var phase = Math.atan2(numeratorImag, numeratorReal) - Math.atan2(denominatorImag, denominatorReal);
|
||||
|
||||
if (phase >= Math.PI) {
|
||||
phase -= 2 * Math.PI;
|
||||
} else if (phase <= -Math.PI) {
|
||||
phase += 2 * Math.PI;
|
||||
}
|
||||
|
||||
return {magnitude : magnitude, phase : phase};
|
||||
}
|
||||
|
||||
// Compute the reference frequency response for the biquad filter |filter| at the frequency samples
|
||||
// given by |frequencies|.
|
||||
function frequencyResponseReference(filter, frequencies)
|
||||
{
|
||||
var sampleRate = filter.context.sampleRate;
|
||||
var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate);
|
||||
var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value);
|
||||
|
||||
var magnitudes = [];
|
||||
var phases = [];
|
||||
|
||||
for (var k = 0; k < frequencies.length; ++k) {
|
||||
var response = getResponseAt(filterCoefficients, normalizedFrequency(frequencies[k], sampleRate));
|
||||
magnitudes.push(response.magnitude);
|
||||
phases.push(response.phase);
|
||||
}
|
||||
|
||||
return {magnitudes : magnitudes, phases : phases};
|
||||
}
|
||||
|
||||
// Compute a set of linearly spaced frequencies.
|
||||
function createFrequencies(nFrequencies, sampleRate)
|
||||
{
|
||||
var frequencies = new Float32Array(nFrequencies);
|
||||
var nyquist = sampleRate / 2;
|
||||
var freqDelta = nyquist / nFrequencies;
|
||||
|
||||
for (var k = 0; k < nFrequencies; ++k) {
|
||||
frequencies[k] = k * freqDelta;
|
||||
}
|
||||
|
||||
return frequencies;
|
||||
}
|
||||
|
||||
function linearToDecibels(x)
|
||||
{
|
||||
if (x) {
|
||||
return 20 * Math.log(x) / Math.LN10;
|
||||
} else {
|
||||
return -1000;
|
||||
}
|
||||
}
|
||||
|
||||
// Look through the array and find any NaN or infinity. Returns the index of the first occurence or
|
||||
// -1 if none.
|
||||
function findBadNumber(signal)
|
||||
{
|
||||
for (var k = 0; k < signal.length; ++k) {
|
||||
if (!isValidNumber(signal[k])) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Compute absolute value of the difference between phase angles, taking into account the wrapping
|
||||
// of phases.
|
||||
function absolutePhaseDifference(x, y)
|
||||
{
|
||||
var diff = Math.abs(x - y);
|
||||
|
||||
if (diff > Math.PI) {
|
||||
diff = 2 * Math.PI - diff;
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Compare the frequency response with our expected response.
|
||||
function compareResponses(filter, frequencies, magResponse, phaseResponse)
|
||||
{
|
||||
var expectedResponse = frequencyResponseReference(filter, frequencies);
|
||||
|
||||
expectedMagnitudes = expectedResponse.magnitudes;
|
||||
expectedPhases = expectedResponse.phases;
|
||||
|
||||
var n = magResponse.length;
|
||||
var success = true;
|
||||
var badResponse = false;
|
||||
|
||||
var maxMagError = -1;
|
||||
var maxMagErrorIndex = -1;
|
||||
|
||||
var k;
|
||||
var hasBadNumber;
|
||||
|
||||
hasBadNumber = findBadNumber(magResponse);
|
||||
if (hasBadNumber >= 0) {
|
||||
testFailed("Magnitude response has NaN or infinity at " + hasBadNumber);
|
||||
success = false;
|
||||
badResponse = true;
|
||||
}
|
||||
|
||||
hasBadNumber = findBadNumber(phaseResponse);
|
||||
if (hasBadNumber >= 0) {
|
||||
testFailed("Phase response has NaN or infinity at " + hasBadNumber);
|
||||
success = false;
|
||||
badResponse = true;
|
||||
}
|
||||
|
||||
// These aren't testing the implementation itself. Instead, these are sanity checks on the
|
||||
// reference. Failure here does not imply an error in the implementation.
|
||||
hasBadNumber = findBadNumber(expectedMagnitudes);
|
||||
if (hasBadNumber >= 0) {
|
||||
testFailed("Expected magnitude response has NaN or infinity at " + hasBadNumber);
|
||||
success = false;
|
||||
badResponse = true;
|
||||
}
|
||||
|
||||
hasBadNumber = findBadNumber(expectedPhases);
|
||||
if (hasBadNumber >= 0) {
|
||||
testFailed("Expected phase response has NaN or infinity at " + hasBadNumber);
|
||||
success = false;
|
||||
badResponse = true;
|
||||
}
|
||||
|
||||
// If we found a NaN or infinity, the following tests aren't very helpful, especially for NaN.
|
||||
// We run them anyway, after printing a warning message.
|
||||
|
||||
if (badResponse) {
|
||||
testFailed("NaN or infinity in the actual or expected results makes the following test results suspect.");
|
||||
success = false;
|
||||
}
|
||||
|
||||
for (k = 0; k < n; ++k) {
|
||||
var error = Math.abs(linearToDecibels(magResponse[k]) - linearToDecibels(expectedMagnitudes[k]));
|
||||
if (error > maxMagError) {
|
||||
maxMagError = error;
|
||||
maxMagErrorIndex = k;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxMagError > maxAllowedMagError) {
|
||||
var message = "Magnitude error (" + maxMagError + " dB)";
|
||||
message += " exceeded threshold at " + frequencies[maxMagErrorIndex];
|
||||
message += " Hz. Actual: " + linearToDecibels(magResponse[maxMagErrorIndex]);
|
||||
message += " dB, expected: " + linearToDecibels(expectedMagnitudes[maxMagErrorIndex]) + " dB.";
|
||||
testFailed(message);
|
||||
success = false;
|
||||
} else {
|
||||
testPassed("Magnitude response within acceptable threshold.");
|
||||
}
|
||||
|
||||
var maxPhaseError = -1;
|
||||
var maxPhaseErrorIndex = -1;
|
||||
|
||||
for (k = 0; k < n; ++k) {
|
||||
var error = absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
|
||||
if (error > maxPhaseError) {
|
||||
maxPhaseError = error;
|
||||
maxPhaseErrorIndex = k;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxPhaseError > maxAllowedPhaseError) {
|
||||
var message = "Phase error (radians) (" + maxPhaseError;
|
||||
message += ") exceeded threshold at " + frequencies[maxPhaseErrorIndex];
|
||||
message += " Hz. Actual: " + phaseResponse[maxPhaseErrorIndex];
|
||||
message += " expected: " + expectedPhases[maxPhaseErrorIndex];
|
||||
testFailed(message);
|
||||
success = false;
|
||||
} else {
|
||||
testPassed("Phase response within acceptable threshold.");
|
||||
}
|
||||
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
function runTest()
|
||||
{
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
context = new AudioContext();
|
||||
|
||||
filter = context.createBiquadFilter();
|
||||
|
||||
// Arbitrarily test a peaking filter, but any kind of filter can be tested.
|
||||
filter.type = "peaking";
|
||||
filter.frequency.value = filterCutoff;
|
||||
filter.Q.value = filterQ;
|
||||
filter.gain.value = filterGain;
|
||||
|
||||
var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
|
||||
magResponse = new Float32Array(numberOfFrequencies);
|
||||
phaseResponse = new Float32Array(numberOfFrequencies);
|
||||
|
||||
filter.getFrequencyResponse(frequencies, magResponse, phaseResponse);
|
||||
var success = compareResponses(filter, frequencies, magResponse, phaseResponse);
|
||||
|
||||
if (success) {
|
||||
testPassed("Frequency response was correct.");
|
||||
} else {
|
||||
testFailed("Frequency response was incorrect.");
|
||||
}
|
||||
|
||||
finishJSTest();
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
46
dom/media/webaudio/test/blink/biquad-highpass.html
Normal file
46
dom/media/webaudio/test/blink/biquad-highpass.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad highpass filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
// The filters we want to test.
|
||||
var filterParameters = [{cutoff : 0, q : 1, gain : 1 },
|
||||
{cutoff : 1, q : 1, gain : 1 },
|
||||
{cutoff : 0.25, q : 1, gain : 1 },
|
||||
];
|
||||
|
||||
createTestAndRun(context, "highpass", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
46
dom/media/webaudio/test/blink/biquad-highshelf.html
Normal file
46
dom/media/webaudio/test/blink/biquad-highshelf.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad highshelf filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
// The filters we want to test.
|
||||
var filterParameters = [{cutoff : 0, q : 10, gain : 10 },
|
||||
{cutoff : 1, q : 10, gain : 10 },
|
||||
{cutoff : 0.25, q : 10, gain : 10 },
|
||||
];
|
||||
|
||||
createTestAndRun(context, "highshelf", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
47
dom/media/webaudio/test/blink/biquad-lowpass.html
Normal file
47
dom/media/webaudio/test/blink/biquad-lowpass.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad lowpass filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
// The filters we want to test.
|
||||
var filterParameters = [{cutoff : 0, q : 1, gain : 1 },
|
||||
{cutoff : 1, q : 1, gain : 1 },
|
||||
{cutoff : 0.25, q : 1, gain : 1 },
|
||||
{cutoff : 0.25, q : 1, gain : 1, detune : 100 },
|
||||
{cutoff : 0.01, q : 1, gain : 1, detune : -200 },
|
||||
];
|
||||
createTestAndRun(context, "lowpass", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
46
dom/media/webaudio/test/blink/biquad-lowshelf.html
Normal file
46
dom/media/webaudio/test/blink/biquad-lowshelf.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad lowshelf filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
// The filters we want to test.
|
||||
var filterParameters = [{cutoff : 0, q : 10, gain : 10 },
|
||||
{cutoff : 1, q : 10, gain : 10 },
|
||||
{cutoff : 0.25, q : 10, gain : 10 },
|
||||
];
|
||||
|
||||
createTestAndRun(context, "lowshelf", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
46
dom/media/webaudio/test/blink/biquad-notch.html
Normal file
46
dom/media/webaudio/test/blink/biquad-notch.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad notch filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
var filterParameters = [{cutoff : 0, q : 10, gain : 1 },
|
||||
{cutoff : 1, q : 10, gain : 1 },
|
||||
{cutoff : .5, q : 0, gain : 1 },
|
||||
{cutoff : 0.25, q : 10, gain : 1 },
|
||||
];
|
||||
|
||||
createTestAndRun(context, "notch", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
47
dom/media/webaudio/test/blink/biquad-peaking.html
Normal file
47
dom/media/webaudio/test/blink/biquad-peaking.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/biquad-filters.js"></script>
|
||||
<script src="resources/biquad-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="description"></div>
|
||||
<div id="console"></div>
|
||||
|
||||
<script>
|
||||
description("Tests Biquad peaking filter.");
|
||||
|
||||
function runTest() {
|
||||
if (window.testRunner) {
|
||||
testRunner.dumpAsText();
|
||||
testRunner.waitUntilDone();
|
||||
}
|
||||
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// Create offline audio context.
|
||||
var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
|
||||
|
||||
// The filters we want to test.
|
||||
var filterParameters = [{cutoff : 0, q : 10, gain : 10 },
|
||||
{cutoff : 1, q : 10, gain : 10 },
|
||||
{cutoff : .5, q : 0, gain : 10 },
|
||||
{cutoff : 0.25, q : 10, gain : 10 },
|
||||
];
|
||||
|
||||
createTestAndRun(context, "peaking", filterParameters);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
67
dom/media/webaudio/test/blink/biquad-tail.html
Normal file
67
dom/media/webaudio/test/blink/biquad-tail.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Biquad Tail Output</title>
|
||||
<script src="../resources/js-test.js"></script>
|
||||
<script src="resources/compatibility.js"></script>
|
||||
<script src="resources/audio-testing.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
description("Test Biquad Tail Output");
|
||||
window.jsTestIsAsync = true;
|
||||
|
||||
// A high sample rate shows the issue more clearly.
|
||||
var sampleRate = 192000;
|
||||
// Some short duration because we don't need to run the test for very long.
|
||||
var testDurationSec = 0.5;
|
||||
var testDurationFrames = testDurationSec * sampleRate;
|
||||
|
||||
// Amplitude experimentally determined to give a biquad output close to 1. (No attempt was
|
||||
// made to produce exactly 1; it's not needed.)
|
||||
var sourceAmplitude = 100;
|
||||
|
||||
// The output of the biquad filter should not change by more than this much between output
|
||||
// samples. Threshold was determined experimentally.
|
||||
var glitchThreshold = 0.0129;
|
||||
|
||||
// Test that a Biquad filter doesn't have it's output terminated because the input has gone
|
||||
// away. Generally, when a source node is finished, it disconnects itself from any downstream
|
||||
// nodes. This is the correct behavior. Nodes that have no inputs (disconnected) are
|
||||
// generally assumed to output zeroes. This is also desired behavior. However, biquad
|
||||
// filters have memory so they should not suddenly output zeroes when the input is
|
||||
// disconnected. This test checks to see if the output doesn't suddenly change to zero.
|
||||
function runTest() {
|
||||
var context = new OfflineAudioContext(1, testDurationFrames, sampleRate);
|
||||
|
||||
// Create an impulse source.
|
||||
var buffer = context.createBuffer(1, 1, context.sampleRate);
|
||||
buffer.getChannelData(0)[0] = sourceAmplitude;
|
||||
var source = context.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
|
||||
// Create the biquad filter. It doesn't really matter what kind, so the default filter type
|
||||
// and parameters is fine. Connect the source to it.
|
||||
var biquad = context.createBiquadFilter();
|
||||
source.connect(biquad);
|
||||
biquad.connect(context.destination);
|
||||
|
||||
source.start();
|
||||
|
||||
context.startRendering().then(function(result) {
|
||||
// There should be no large discontinuities in the output
|
||||
var success = true;
|
||||
success = success && Should("Biquad output", result.getChannelData(0)).notGlitch(glitchThreshold);
|
||||
if (success)
|
||||
testPassed("Biquad tail output correctly completed.");
|
||||
else
|
||||
testFailed("Biquad tail output not correctly completed.");
|
||||
}).then(finishJSTest);
|
||||
}
|
||||
|
||||
runTest();
|
||||
successfullyParsed = true;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
179
dom/media/webaudio/test/blink/biquad-testing.js
Normal file
179
dom/media/webaudio/test/blink/biquad-testing.js
Normal file
@ -0,0 +1,179 @@
|
||||
// Globals, to make testing and debugging easier.
|
||||
var context;
|
||||
var filter;
|
||||
var signal;
|
||||
var renderedBuffer;
|
||||
var renderedData;
|
||||
|
||||
var sampleRate = 44100.0;
|
||||
var pulseLengthFrames = .1 * sampleRate;
|
||||
|
||||
// Maximum allowed error for the test to succeed. Experimentally determined.
|
||||
var maxAllowedError = 5.9e-8;
|
||||
|
||||
// This must be large enough so that the filtered result is
|
||||
// essentially zero. See comments for createTestAndRun.
|
||||
var timeStep = .1;
|
||||
|
||||
// Maximum number of filters we can process (mostly for setting the
|
||||
// render length correctly.)
|
||||
var maxFilters = 5;
|
||||
|
||||
// How long to render. Must be long enough for all of the filters we
|
||||
// want to test.
|
||||
var renderLengthSeconds = timeStep * (maxFilters + 1) ;
|
||||
|
||||
var renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);
|
||||
|
||||
// Number of filters that will be processed.
|
||||
var nFilters;
|
||||
|
||||
function createImpulseBuffer(context, length) {
|
||||
var impulse = context.createBuffer(1, length, context.sampleRate);
|
||||
var data = impulse.getChannelData(0);
|
||||
for (var k = 1; k < data.length; ++k) {
|
||||
data[k] = 0;
|
||||
}
|
||||
data[0] = 1;
|
||||
|
||||
return impulse;
|
||||
}
|
||||
|
||||
|
||||
function createTestAndRun(context, filterType, filterParameters) {
|
||||
// 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.
|
||||
|
||||
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 (var 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);
|
||||
}
|
||||
|
||||
context.oncomplete = checkFilterResponse(filterType, filterParameters);
|
||||
context.startRendering();
|
||||
}
|
||||
|
||||
function addSignal(dest, src, destOffset) {
|
||||
// Add src to dest at the given dest offset.
|
||||
for (var k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
|
||||
dest[k] += src[j];
|
||||
}
|
||||
}
|
||||
|
||||
function generateReference(filterType, filterParameters) {
|
||||
var result = new Array(renderLengthSamples);
|
||||
var data = new Array(renderLengthSamples);
|
||||
// Initialize the result array and data.
|
||||
for (var k = 0; k < result.length; ++k) {
|
||||
result[k] = 0;
|
||||
data[k] = 0;
|
||||
}
|
||||
// Make data an impulse.
|
||||
data[0] = 1;
|
||||
|
||||
for (var k = 0; k < nFilters; ++k) {
|
||||
// Filter an impulse
|
||||
var detune = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
|
||||
var frequency = filterParameters[k].cutoff * Math.pow(2, detune / 1200); // Apply detune, converting from Cents.
|
||||
|
||||
var filterCoef = createFilter(filterType,
|
||||
frequency,
|
||||
filterParameters[k].q,
|
||||
filterParameters[k].gain);
|
||||
var 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(filterType, filterParameters) {
|
||||
return function(event) {
|
||||
renderedBuffer = event.renderedBuffer;
|
||||
renderedData = renderedBuffer.getChannelData(0);
|
||||
|
||||
reference = generateReference(filterType, filterParameters);
|
||||
|
||||
var len = Math.min(renderedData.length, reference.length);
|
||||
|
||||
var success = true;
|
||||
|
||||
// Maximum error between rendered data and expected data
|
||||
var maxError = 0;
|
||||
|
||||
// Sample offset where the maximum error occurred.
|
||||
var maxPosition = 0;
|
||||
|
||||
// Number of infinities or NaNs that occurred in the rendered data.
|
||||
var invalidNumberCount = 0;
|
||||
|
||||
if (nFilters != filterParameters.length) {
|
||||
testFailed("Test wanted " + filterParameters.length + " filters but only " + maxFilters + " allowed.");
|
||||
success = false;
|
||||
}
|
||||
|
||||
// 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 (var k = 0; k < len; ++k) {
|
||||
var err = Math.abs(renderedData[k] - reference[k]);
|
||||
if (err > maxError) {
|
||||
maxError = err;
|
||||
maxPosition = k;
|
||||
}
|
||||
if (!isValidNumber(renderedData[k])) {
|
||||
++invalidNumberCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidNumberCount > 0) {
|
||||
testFailed("Rendered output has " + invalidNumberCount + " infinities or NaNs.");
|
||||
success = false;
|
||||
} else {
|
||||
testPassed("Rendered output did not have infinities or NaNs.");
|
||||
}
|
||||
|
||||
if (maxError <= maxAllowedError) {
|
||||
testPassed(filterTypeName[filterType] + " response is correct.");
|
||||
} else {
|
||||
testFailed(filterTypeName[filterType] + " response is incorrect. Max err = " + maxError + " at " + maxPosition + ". Threshold = " + maxAllowedError);
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
testPassed("Test signal was correctly filtered.");
|
||||
} else {
|
||||
testFailed("Test signal was not correctly filtered.");
|
||||
}
|
||||
finishJSTest();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user