summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html
blob: 23222e4df9698d408b886ea108cc4c732a7d61fe (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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
<!DOCTYPE html>
<html>
  <head>
    <title>
      Test BiquadFilter getFrequencyResponse() functionality
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
    <script src="/webaudio/resources/biquad-filters.js"></script>
    <script src="/webaudio/resources/biquad-testing.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let audit = Audit.createTaskRunner();

      // 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.

      // The magnitude response of the biquad filter.
      let magResponse;

      // The phase response of the biquad filter.
      let phaseResponse;

      // Number of frequency samples to take.
      let numberOfFrequencies = 1000;

      // The filter parameters.
      let filterCutoff = 1000;  // Hz.
      let filterQ = 1;
      let filterGain = 5;  // Decibels.

      // The magnitudes and phases of the reference frequency response.
      let expectedMagnitudes;
      let expectedPhases;

      // Convert frequency in Hz to a normalized frequency between 0 to 1 with 1
      // corresponding to the Nyquist frequency.
      function normalizedFrequency(freqHz, sampleRate) {
        let 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) {
        let b0 = coef.b0;
        let b1 = coef.b1;
        let b2 = coef.b2;
        let a1 = coef.a1;
        let 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.

        let omega = Math.PI * f;
        let numeratorReal =
            b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
        let numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
        let denominatorReal =
            1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
        let denominatorImag =
            -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));

        let magnitude = Math.sqrt(
            (numeratorReal * numeratorReal + numeratorImag * numeratorImag) /
            (denominatorReal * denominatorReal +
             denominatorImag * denominatorImag));
        let 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) {
        let sampleRate = filter.context.sampleRate;
        let normalizedFreq =
            normalizedFrequency(filter.frequency.value, sampleRate);
        let filterCoefficients = createFilter(
            filter.type, normalizedFreq, filter.Q.value, filter.gain.value);

        let magnitudes = [];
        let phases = [];

        for (let k = 0; k < frequencies.length; ++k) {
          let 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) {
        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;
      }

      function linearToDecibels(x) {
        if (x) {
          return 20 * Math.log(x) / Math.LN10;
        } else {
          return -1000;
        }
      }

      function decibelsToLinear(x) {
        return Math.pow(10, x/20);
      }

      // 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 (let 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) {
        let diff = Math.abs(x - y);

        if (diff > Math.PI) {
          diff = 2 * Math.PI - diff;
        }
        return diff;
      }

      // Compare the frequency response with our expected response.
      //
      //   should - The |should| method provided by audit.define
      //   filter - The filter node used in the test
      //   frequencies - array of frequencies provided to |getFrequencyResponse|
      //   magResponse - mag response from |getFrequencyResponse|
      //   phaseResponse - phase response from |getFrequencyResponse|
      //   maxAllowedMagError - error threshold for mag response, in dB
      //   maxAllowedPhaseError - error threshold for phase response, in rad.
      function compareResponses(
          should, filter, frequencies, magResponse, phaseResponse,
          maxAllowedMagError, maxAllowedPhaseError) {
        let expectedResponse = frequencyResponseReference(filter, frequencies);

        expectedMagnitudes = expectedResponse.magnitudes;
        expectedPhases = expectedResponse.phases;

        let n = magResponse.length;
        let badResponse = false;

        let maxMagError = -1;
        let maxMagErrorIndex = -1;

        let k;
        let hasBadNumber;

        hasBadNumber = findBadNumber(magResponse);
        badResponse =
            !should(
                 hasBadNumber >= 0 ? 1 : 0,
                 filter.type +
                     ': Number of non-finite values in magnitude response')
                 .beEqualTo(0);

        hasBadNumber = findBadNumber(phaseResponse);
        badResponse =
            !should(
                 hasBadNumber >= 0 ? 1 : 0,
                 filter.type + ': Number of non-finte values in phase response')
                 .beEqualTo(0);

        // 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);
        badResponse =
            !should(
                 hasBadNumber >= 0 ? 1 : 0,
                 filter.type +
                     ': Number of non-finite values in the expected magnitude response')
                 .beEqualTo(0);

        hasBadNumber = findBadNumber(expectedPhases);
        badResponse =
            !should(
                 hasBadNumber >= 0 ? 1 : 0,
                 filter.type +
                     ': Number of non-finite values in expected phase response')
                 .beEqualTo(0);

        // 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.
        should(
            !badResponse,
            filter.type +
                ': Actual and expected results contained only finite values')
            .beTrue();

        for (k = 0; k < n; ++k) {
          let error = Math.abs(
              linearToDecibels(magResponse[k]) -
              linearToDecibels(expectedMagnitudes[k]));
          if (error > maxMagError) {
            maxMagError = error;
            maxMagErrorIndex = k;
          }
        }

        should(
            linearToDecibels(maxMagError),
            filter.type + ': Max error (' + linearToDecibels(maxMagError) +
                ' dB) of magnitude response at frequency ' +
                frequencies[maxMagErrorIndex] + ' Hz')
            .beLessThanOrEqualTo(linearToDecibels(maxAllowedMagError));
        let maxPhaseError = -1;
        let maxPhaseErrorIndex = -1;

        for (k = 0; k < n; ++k) {
          let error =
              absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
          if (error > maxPhaseError) {
            maxPhaseError = error;
            maxPhaseErrorIndex = k;
          }
        }

        should(
            radToDegree(maxPhaseError),
            filter.type + ': Max error (' + radToDegree(maxPhaseError) +
                ' deg) in phase response at frequency ' +
                frequencies[maxPhaseErrorIndex] + ' Hz')
            .beLessThanOrEqualTo(radToDegree(maxAllowedPhaseError));
      }

      function radToDegree(rad) {
        // Radians to degrees
        return rad * 180 / Math.PI;
      }

      // Test the getFrequencyResponse for each of filter types.  Each entry in
      // this array is a dictionary with these elements:
      //
      //   type:  filter type to be tested
      //   maxErrorInMagnitude:  Allowed error in computed magnitude response
      //   maxErrorInPhase:      Allowed error in computed magnitude phase
      [{
        type: 'lowpass',
        maxErrorInMagnitude: decibelsToLinear(-73.0178),
        maxErrorInPhase: 8.04360e-6
      },
       {
         type: 'highpass',
         maxErrorInMagnitude: decibelsToLinear(-117.5461),
         maxErrorInPhase: 6.9691e-6
       },
       {
         type: 'bandpass',
         maxErrorInMagnitude: decibelsToLinear(-79.0139),
         maxErrorInPhase: 4.9371e-6
       },
       {
         type: 'lowshelf',
         maxErrorInMagnitude: decibelsToLinear(-120.4038),
         maxErrorInPhase: 4.0724e-6
       },
       {
         type: 'highshelf',
         maxErrorInMagnitude: decibelsToLinear(-120, 1303),
         maxErrorInPhase: 4.0724e-6
       },
       {
         type: 'peaking',
         maxErrorInMagnitude: decibelsToLinear(-119.1176),
         maxErrorInPhase: 6.4724e-8
       },
       {
         type: 'notch',
         maxErrorInMagnitude: decibelsToLinear(-87.0808),
         maxErrorInPhase: 6.6300e-6
       },
       {
         type: 'allpass',
         maxErrorInMagnitude: decibelsToLinear(-265.3517),
         maxErrorInPhase: 1.3260e-5
       }].forEach(test => {
        audit.define(
            {label: test.type, description: 'Frequency response'},
            (task, should) => {
              let context = new AudioContext();

              let filter = new BiquadFilterNode(context, {
                type: test.type,
                frequency: filterCutoff,
                Q: filterQ,
                gain: filterGain
              });

              let frequencies =
                  createFrequencies(numberOfFrequencies, context.sampleRate);
              magResponse = new Float32Array(numberOfFrequencies);
              phaseResponse = new Float32Array(numberOfFrequencies);

              filter.getFrequencyResponse(
                  frequencies, magResponse, phaseResponse);
              compareResponses(
                  should, filter, frequencies, magResponse, phaseResponse,
                  test.maxErrorInMagnitude, test.maxErrorInPhase);

              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 BiquadFilterNode(context);

            // 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>