Skip to content

Commit dd3732b

Browse files
committed
Use stable Zero-Delay Feedback (ZDF) SVF topology for filters
1 parent d18bd72 commit dd3732b

6 files changed

Lines changed: 100 additions & 32 deletions

File tree

src/domain/devices/high_pass_filter_effect.cpp

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,48 @@ void HighPassFilterEffect::process(float & left, float & right, uint32_t sampleR
3232
return;
3333
}
3434

35+
// Zero-Delay Feedback State Variable Filter (2nd order)
3536
const float freq = 20.0f * std::pow(std::min(20000.0f, sampleRate * 0.49f) / 20.0f, m_cutoff);
36-
const float f = std::min(1.0f, 2.0f * std::sin(std::numbers::pi_v<float> * freq / static_cast<float>(sampleRate)));
37-
const float q = 0.5f;
38-
39-
m_hpL = left - m_lpL - q * m_bpL;
40-
m_bpL += f * m_hpL;
41-
m_lpL += f * m_bpL;
42-
left = m_hpL;
43-
44-
m_hpR = right - m_lpR - q * m_bpR;
45-
m_bpR += f * m_hpR;
46-
m_lpR += f * m_bpR;
47-
right = m_hpR;
37+
const double g = std::tan(std::numbers::pi * static_cast<double>(freq) / static_cast<double>(sampleRate));
38+
const double k = 1.0; // Q = 1.0 / k
39+
const double damping = 1.0 / (1.0 + g * (g + k));
40+
41+
// Left channel
42+
{
43+
const double hp = (static_cast<double>(left) - (g + k) * m_s1L - m_s2L) * damping;
44+
const double v1 = g * hp;
45+
const double v = v1 + m_s1L;
46+
m_s1L = v1 + v;
47+
const double v2 = g * v;
48+
const double lp = v2 + m_s2L;
49+
m_s2L = v2 + lp;
50+
left = static_cast<float>(hp);
51+
}
52+
53+
// Right channel
54+
{
55+
const double hp = (static_cast<double>(right) - (g + k) * m_s1R - m_s2R) * damping;
56+
const double v1 = g * hp;
57+
const double v = v1 + m_s1R;
58+
m_s1R = v1 + v;
59+
const double v2 = g * v;
60+
const double lp = v2 + m_s2R;
61+
m_s2R = v2 + lp;
62+
right = static_cast<float>(hp);
63+
}
64+
65+
// NaN protection
66+
if (std::isnan(left) || std::isnan(right)) {
67+
reset();
68+
left = 0.0f;
69+
right = 0.0f;
70+
}
4871
}
4972

5073
void HighPassFilterEffect::reset()
5174
{
52-
m_lpL = m_hpL = m_bpL = 0.0f;
53-
m_lpR = m_hpR = m_bpR = 0.0f;
75+
m_s1L = m_s2L = 0.0;
76+
m_s1R = m_s2R = 0.0;
5477
}
5578

5679
} // namespace noteahead

src/domain/devices/high_pass_filter_effect.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class HighPassFilterEffect : public Effect
2929

3030
private:
3131
float m_cutoff { 0.0f };
32-
float m_lpL { 0.0f }, m_hpL { 0.0f }, m_bpL { 0.0f };
33-
float m_lpR { 0.0f }, m_hpR { 0.0f }, m_bpR { 0.0f };
32+
double m_s1L { 0.0 }, m_s2L { 0.0 };
33+
double m_s1R { 0.0 }, m_s2R { 0.0 };
3434
};
3535

3636
} // namespace noteahead

src/domain/devices/low_pass_filter_effect.cpp

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,48 @@ void LowPassFilterEffect::process(float & left, float & right, uint32_t sampleRa
3232
return;
3333
}
3434

35+
// Zero-Delay Feedback State Variable Filter (2nd order)
3536
const float freq = 20.0f * std::pow(std::min(20000.0f, sampleRate * 0.49f) / 20.0f, m_cutoff);
36-
const float f = 2.0f * std::sin(std::numbers::pi_v<float> * freq / static_cast<float>(sampleRate));
37-
const float q = 0.5f;
38-
39-
m_hpL = left - m_lpL - q * m_bpL;
40-
m_bpL += f * m_hpL;
41-
m_lpL += f * m_bpL;
42-
left = m_lpL;
43-
44-
m_hpR = right - m_lpR - q * m_bpR;
45-
m_bpR += f * m_hpR;
46-
m_lpR += f * m_bpR;
47-
right = m_lpR;
37+
const double g = std::tan(std::numbers::pi * static_cast<double>(freq) / static_cast<double>(sampleRate));
38+
const double k = 1.0; // Q = 1.0 / k
39+
const double damping = 1.0 / (1.0 + g * (g + k));
40+
41+
// Left channel
42+
{
43+
const double hp = (static_cast<double>(left) - (g + k) * m_s1L - m_s2L) * damping;
44+
const double v1 = g * hp;
45+
const double v = v1 + m_s1L;
46+
m_s1L = v1 + v;
47+
const double v2 = g * v;
48+
const double lp = v2 + m_s2L;
49+
m_s2L = v2 + lp;
50+
left = static_cast<float>(lp);
51+
}
52+
53+
// Right channel
54+
{
55+
const double hp = (static_cast<double>(right) - (g + k) * m_s1R - m_s2R) * damping;
56+
const double v1 = g * hp;
57+
const double v = v1 + m_s1R;
58+
m_s1R = v1 + v;
59+
const double v2 = g * v;
60+
const double lp = v2 + m_s2R;
61+
m_s2R = v2 + lp;
62+
right = static_cast<float>(lp);
63+
}
64+
65+
// NaN protection
66+
if (std::isnan(left) || std::isnan(right)) {
67+
reset();
68+
left = 0.0f;
69+
right = 0.0f;
70+
}
4871
}
4972

5073
void LowPassFilterEffect::reset()
5174
{
52-
m_lpL = m_hpL = m_bpL = 0.0f;
53-
m_lpR = m_hpR = m_bpR = 0.0f;
75+
m_s1L = m_s2L = 0.0;
76+
m_s1R = m_s2R = 0.0;
5477
}
5578

5679
} // namespace noteahead

src/domain/devices/low_pass_filter_effect.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class LowPassFilterEffect : public Effect
2929

3030
private:
3131
float m_cutoff { 1.0f };
32-
float m_lpL { 0.0f }, m_hpL { 0.0f }, m_bpL { 0.0f };
33-
float m_lpR { 0.0f }, m_hpR { 0.0f }, m_bpR { 0.0f };
32+
double m_s1L { 0.0 }, m_s2L { 0.0 };
33+
double m_s1R { 0.0 }, m_s2R { 0.0 };
3434
};
3535

3636
} // namespace noteahead

src/unit_tests/effects_test/effects_test.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,27 @@ void EffectsTest::test_highPassFilterEffect()
139139
}
140140
}
141141

142+
void EffectsTest::test_filterStability()
143+
{
144+
LowPassFilterEffect lp;
145+
HighPassFilterEffect hp;
146+
147+
for (int i = 0; i < 1000; ++i) {
148+
float left = 1.0f;
149+
float right = 1.0f;
150+
const float cutoff = 0.5f + 0.49f * std::sin(i * 0.1f);
151+
152+
lp.setCutoff(cutoff);
153+
hp.setCutoff(cutoff);
154+
155+
lp.process(left, right, 44100);
156+
hp.process(left, right, 44100);
157+
158+
QVERIFY(!std::isnan(left));
159+
QVERIFY(!std::isnan(right));
160+
}
161+
}
162+
142163
} // namespace noteahead
143164

144165
QTEST_GUILESS_MAIN(noteahead::EffectsTest)

src/unit_tests/effects_test/effects_test.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ private slots:
2929
void test_panningEffect();
3030
void test_lowPassFilterEffect();
3131
void test_highPassFilterEffect();
32+
void test_filterStability();
3233
};
3334

3435
} // namespace noteahead

0 commit comments

Comments
 (0)