summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/resources/biquad-testing.js
blob: 7f90a1f72bec6be2cfc3a1256a40132958632ca9 (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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// Globals, to make testing and debugging easier.
let context;
let filter;
let signal;
let renderedBuffer;
let renderedData;

// Use a power of two to eliminate round-off in converting frame to time
let sampleRate = 32768;
let pulseLengthFrames = .1 * sampleRate;

// Maximum allowed error for the test to succeed.  Experimentally determined.
let maxAllowedError = 5.9e-8;

// This must be large enough so that the filtered result is essentially zero.
// See comments for createTestAndRun.  This must be a whole number of frames.
let timeStep = Math.ceil(.1 * sampleRate) / sampleRate;

// Maximum number of filters we can process (mostly for setting the
// render length correctly.)
let maxFilters = 5;

// How long to render.  Must be long enough for all of the filters we
// want to test.
let renderLengthSeconds = timeStep * (maxFilters + 1);

let renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);

// Number of filters that will be processed.
let nFilters;

function createImpulseBuffer(context, length) {
  let impulse = context.createBuffer(1, length, context.sampleRate);
  let data = impulse.getChannelData(0);
  for (let k = 1; k < data.length; ++k) {
    data[k] = 0;
  }
  data[0] = 1;

  return impulse;
}


function createTestAndRun(context, filterType, testParameters) {
  // 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.

  let filterParameters = testParameters.filterParameters;
  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 (let 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);
  }

  return context.startRendering().then(buffer => {
    checkFilterResponse(buffer, filterType, testParameters);
  });
}

function addSignal(dest, src, destOffset) {
  // Add src to dest at the given dest offset.
  for (let k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
    dest[k] += src[j];
  }
}

function generateReference(filterType, filterParameters) {
  let result = new Array(renderLengthSamples);
  let data = new Array(renderLengthSamples);
  // Initialize the result array and data.
  for (let k = 0; k < result.length; ++k) {
    result[k] = 0;
    data[k] = 0;
  }
  // Make data an impulse.
  data[0] = 1;

  for (let k = 0; k < nFilters; ++k) {
    // Filter an impulse
    let detune = (filterParameters[k].detune === undefined) ?
        0 :
        filterParameters[k].detune;
    let frequency = filterParameters[k].cutoff *
        Math.pow(2, detune / 1200);  // Apply detune, converting from Cents.

    let filterCoef = createFilter(
        filterType, frequency, filterParameters[k].q, filterParameters[k].gain);
    let 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(renderedBuffer, filterType, testParameters) {
  let filterParameters = testParameters.filterParameters;
  let maxAllowedError = testParameters.threshold;
  let should = testParameters.should;

  renderedData = renderedBuffer.getChannelData(0);

  reference = generateReference(filterType, filterParameters);

  let len = Math.min(renderedData.length, reference.length);

  let success = true;

  // Maximum error between rendered data and expected data
  let maxError = 0;

  // Sample offset where the maximum error occurred.
  let maxPosition = 0;

  // Number of infinities or NaNs that occurred in the rendered data.
  let invalidNumberCount = 0;

  should(nFilters, 'Number of filters tested')
      .beEqualTo(filterParameters.length);

  // 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 (let k = 0; k < len; ++k) {
    let err = Math.abs(renderedData[k] - reference[k]);
    if (err > maxError) {
      maxError = err;
      maxPosition = k;
    }
    if (!isValidNumber(renderedData[k])) {
      ++invalidNumberCount;
    }
  }

  should(
      invalidNumberCount, 'Number of non-finite values in the rendered output')
      .beEqualTo(0);

  should(maxError, 'Max error in ' + filterTypeName[filterType] + ' response')
      .beLessThanOrEqualTo(maxAllowedError);
}