summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/test/blink/biquad-testing.js
blob: 795adf6012bbd3c1dc3714706e48b5e0a4be156f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 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;

        ok(nFilters == filterParameters.length, "Test wanted " + filterParameters.length + " filters but only " + maxFilters + " allowed.");

        compareChannels(renderedData, reference, len, 0, 0, true);

        // Check for bad numbers in the rendered output too.
        // There shouldn't be any.
        for (var k = 0; k < len; ++k) {
            if (!isValidNumber(renderedData[k])) {
                ++invalidNumberCount;
            }
        }

        ok(invalidNumberCount == 0, "Rendered output has " + invalidNumberCount + " infinities or NaNs.");
        SimpleTest.finish();
    }
}