Skip to content

Commit 3f12af9

Browse files
committed
test: add tests for getFrequencyResponse
1 parent 2d69276 commit 3f12af9

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#include <audioapi/core/OfflineAudioContext.h>
2+
#include <audioapi/core/effects/IIRFilterNode.h>
3+
#include <audioapi/core/utils/worklets/SafeIncludes.h>
4+
#include <gtest/gtest.h>
5+
#include <test/src/MockAudioEventHandlerRegistry.h>
6+
#include <algorithm>
7+
#include <complex>
8+
#include <numbers>
9+
10+
using namespace audioapi;
11+
12+
class IIRFilterTest : public ::testing::Test {
13+
protected:
14+
std::shared_ptr<MockAudioEventHandlerRegistry> eventRegistry;
15+
std::unique_ptr<OfflineAudioContext> context;
16+
static constexpr int sampleRate = 44100;
17+
static constexpr float nyquistFrequency = sampleRate / 2.0f;
18+
static constexpr float tolerance = 0.0001f;
19+
20+
void SetUp() override {
21+
eventRegistry = std::make_shared<MockAudioEventHandlerRegistry>();
22+
context = std::make_unique<OfflineAudioContext>(
23+
2, 5 * sampleRate, sampleRate, eventRegistry, RuntimeRegistry{});
24+
}
25+
26+
static std::complex<double> evaluatePolynomial(
27+
std::span<const double> coefficients,
28+
std::complex<double> z,
29+
int order) {
30+
// Use Horner's method to evaluate the polynomial P(z) = sum(coef[k]*z^k, k, 0, order);
31+
std::complex<double> result = 0;
32+
for (int k = order; k >= 0; --k)
33+
result = result * z + std::complex<double>(coefficients[k]);
34+
return result;
35+
}
36+
37+
static void getFrequencyResponseChromium(
38+
std::vector<float> feedforward,
39+
std::vector<float> feedback,
40+
unsigned length,
41+
std::span<const float> frequency,
42+
std::span<float> magResponse,
43+
std::span<float> phaseResponse,
44+
float nyquistFrequency) {
45+
assert(!frequency.empty());
46+
assert(!magResponse.empty());
47+
assert(!phaseResponse.empty());
48+
49+
std::vector<double> m_feedforward(feedforward.begin(), feedforward.end());
50+
std::vector<double> m_feedback(feedback.begin(), feedback.end());
51+
52+
std::vector<float> normalizedFreq(frequency.size());
53+
for (size_t i = 0; i < frequency.size(); ++i) {
54+
normalizedFreq[i] = frequency[i] / nyquistFrequency;
55+
}
56+
57+
// Evaluate the z-transform of the filter at the given normalized frequencies
58+
// from 0 to 1. (One corresponds to the Nyquist frequency.)
59+
//
60+
// The z-tranform of the filter is
61+
//
62+
// H(z) = sum(b[k]*z^(-k), k, 0, M) / sum(a[k]*z^(-k), k, 0, N);
63+
//
64+
// The desired frequency response is H(exp(j*omega)), where omega is in [0,
65+
// 1).
66+
//
67+
// Let P(x) = sum(c[k]*x^k, k, 0, P) be a polynomial of order P. Then each of
68+
// the sums in H(z) is equivalent to evaluating a polynomial at the point
69+
// 1/z.
70+
71+
for (unsigned k = 0; k < length; ++k) {
72+
if (normalizedFreq[k] < 0 || normalizedFreq[k] > 1) {
73+
// Out-of-bounds frequencies should return NaN.
74+
magResponse[k] = std::nanf("");
75+
phaseResponse[k] = std::nanf("");
76+
} else {
77+
// zRecip = 1/z = exp(-j*frequency)
78+
double omega = -std::numbers::pi * normalizedFreq[k];
79+
auto zRecip = std::complex<double>(cos(omega), sin(omega));
80+
81+
auto numerator =
82+
evaluatePolynomial(m_feedforward, zRecip, m_feedforward.size() - 1);
83+
auto denominator =
84+
evaluatePolynomial(m_feedback, zRecip, m_feedback.size() - 1);
85+
auto response = numerator / denominator;
86+
magResponse[k] = static_cast<float>(std::abs(response));
87+
phaseResponse[k] =
88+
static_cast<float>(atan2(imag(response), real(response)));
89+
}
90+
}
91+
}
92+
};
93+
94+
TEST_F(IIRFilterTest, GetFrequencyResponse) {
95+
const std::vector<float> feedforward = {
96+
0.0050662636, 0.0101325272, 0.0050662636};
97+
const std::vector<float> feedback = {
98+
1.0632762845, -1.9797349456, 0.9367237155};
99+
100+
auto node =
101+
std::make_shared<IIRFilterNode>(context.get(), feedforward, feedback);
102+
103+
float frequency = 1000.0f;
104+
float normalizedFrequency = frequency / nyquistFrequency;
105+
106+
std::vector<float> TestFrequencies = {
107+
-0.0001f,
108+
0.0f,
109+
0.0001f,
110+
0.25f * nyquistFrequency,
111+
0.5f * nyquistFrequency,
112+
0.75f * nyquistFrequency,
113+
nyquistFrequency - 0.0001f,
114+
nyquistFrequency,
115+
nyquistFrequency + 0.0001f};
116+
117+
std::vector<float> magResponseNode(TestFrequencies.size());
118+
std::vector<float> phaseResponseNode(TestFrequencies.size());
119+
std::vector<float> magResponseExpected(TestFrequencies.size());
120+
std::vector<float> phaseResponseExpected(TestFrequencies.size());
121+
122+
node->getFrequencyResponse(
123+
TestFrequencies.data(),
124+
magResponseNode.data(),
125+
phaseResponseNode.data(),
126+
TestFrequencies.size());
127+
getFrequencyResponseChromium(
128+
feedforward,
129+
feedback,
130+
TestFrequencies.size(),
131+
TestFrequencies,
132+
magResponseExpected,
133+
phaseResponseExpected,
134+
nyquistFrequency);
135+
136+
for (size_t i = 0; i < TestFrequencies.size(); ++i) {
137+
float f = TestFrequencies[i];
138+
if (std::isnan(magResponseExpected[i])) {
139+
EXPECT_TRUE(std::isnan(magResponseNode[i]))
140+
<< "Expected NaN at frequency " << f;
141+
} else {
142+
EXPECT_NEAR(magResponseNode[i], magResponseExpected[i], tolerance)
143+
<< "Magnitude mismatch at " << f << " Hz";
144+
}
145+
146+
if (std::isnan(phaseResponseExpected[i])) {
147+
EXPECT_TRUE(std::isnan(phaseResponseNode[i]))
148+
<< "Expected NaN at frequency " << f;
149+
} else {
150+
EXPECT_NEAR(phaseResponseNode[i], phaseResponseExpected[i], tolerance)
151+
<< "Phase mismatch at " << f << " Hz";
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)