Skip to content

Commit eb1a9fc

Browse files
committed
Fix biquad tests
1 parent 00871a0 commit eb1a9fc

File tree

2 files changed

+205
-326
lines changed

2 files changed

+205
-326
lines changed

tests/yup_dsp/yup_BiquadCascade.cpp

Lines changed: 205 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323

2424
#include <gtest/gtest.h>
2525

26-
#if 0
27-
2826
using namespace yup;
2927

3028
namespace
@@ -52,8 +50,8 @@ class BiquadCascadeFilterTests : public ::testing::Test
5250

5351
for (int i = 0; i < blockSize; ++i)
5452
{
55-
testData[i] = static_cast<float> (i) / blockSize - 0.5f;
56-
doubleTestData[i] = static_cast<double> (i) / blockSize - 0.5;
53+
testData[i] = 0.1f * std::sin (2.0f * MathConstants<float>::pi * 1000.0f * i / static_cast<float> (sampleRate));
54+
doubleTestData[i] = static_cast<double> (testData[i]);
5755
}
5856
}
5957

@@ -79,25 +77,33 @@ TEST_F (BiquadCascadeFilterTests, ConstructorWithSectionsInitializes)
7977
EXPECT_EQ (4, cascade.getNumSections());
8078
}
8179

82-
TEST_F (BiquadCascadeFilterTests, SetNumSectionsChangesSize)
80+
TEST_F (BiquadCascadeFilterTests, SectionManagement)
8381
{
84-
EXPECT_EQ (2, cascadeFloat.getNumSections());
85-
86-
cascadeFloat.setNumSections (5);
87-
EXPECT_EQ (5, cascadeFloat.getNumSections());
88-
89-
cascadeFloat.setNumSections (1);
90-
EXPECT_EQ (1, cascadeFloat.getNumSections());
82+
cascadeFloat.setNumSections (3);
83+
EXPECT_EQ (cascadeFloat.getNumSections(), 3u);
84+
85+
// Set coefficients for each section
86+
auto coeffs1 = FilterDesigner<double>::designRbjLowpass (500.0, 0.707, sampleRate);
87+
auto coeffs2 = FilterDesigner<double>::designRbjBandpass (1000.0, 2.0, sampleRate);
88+
auto coeffs3 = FilterDesigner<double>::designRbjHighpass (2000.0, 0.707, sampleRate);
89+
90+
cascadeFloat.setSectionCoefficients (0, coeffs1);
91+
cascadeFloat.setSectionCoefficients (1, coeffs2);
92+
cascadeFloat.setSectionCoefficients (2, coeffs3);
93+
94+
// Verify coefficients were set correctly
95+
auto retrievedCoeffs1 = cascadeFloat.getSectionCoefficients (0);
96+
EXPECT_FLOAT_EQ (retrievedCoeffs1.b0, coeffs1.b0);
97+
EXPECT_FLOAT_EQ (retrievedCoeffs1.a1, coeffs1.a1);
9198
}
9299

93100
TEST_F (BiquadCascadeFilterTests, SetAndGetSectionCoefficients)
94101
{
95102
// Create lowpass coefficients
96-
BiquadCascade<float> defaultCascade;
97103
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
98104

99-
defaultCascade.setSectionCoefficients (0, coeffs);
100-
auto retrievedCoeffs = defaultCascade.getSectionCoefficients (0);
105+
cascadeFloat.setSectionCoefficients (0, coeffs);
106+
auto retrievedCoeffs = cascadeFloat.getSectionCoefficients (0);
101107

102108
EXPECT_NEAR (coeffs.b0, retrievedCoeffs.b0, tolerance);
103109
EXPECT_NEAR (coeffs.b1, retrievedCoeffs.b1, tolerance);
@@ -108,29 +114,60 @@ TEST_F (BiquadCascadeFilterTests, SetAndGetSectionCoefficients)
108114

109115
TEST_F (BiquadCascadeFilterTests, InvalidSectionIndexHandling)
110116
{
111-
BiquadCascade<float> defaultCascade;
112117
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
113118

114119
// Should not crash with invalid index
115-
defaultCascade.setSectionCoefficients (999, coeffs);
120+
cascadeFloat.setSectionCoefficients (999, coeffs);
116121

117122
// Should return empty coefficients for invalid index
118-
auto emptyCoeffs = defaultCascade.getSectionCoefficients (999);
123+
auto emptyCoeffs = cascadeFloat.getSectionCoefficients (999);
119124
EXPECT_EQ (1.0, emptyCoeffs.b0); // Default biquad passes through (b0=1)
120125
EXPECT_EQ (0.0, emptyCoeffs.b1);
121126
EXPECT_EQ (0.0, emptyCoeffs.b2);
122127
EXPECT_EQ (0.0, emptyCoeffs.a1);
123128
EXPECT_EQ (0.0, emptyCoeffs.a2);
124129
}
125130

131+
TEST_F (BiquadCascadeFilterTests, InvalidSectionAccess)
132+
{
133+
cascadeFloat.setNumSections (2);
134+
135+
// Trying to access section 5 when only 2 sections exist should not crash
136+
auto coeffs = cascadeFloat.getSectionCoefficients (5);
137+
// Should return default/empty coefficients
138+
EXPECT_TRUE (std::isfinite (coeffs.b0));
139+
}
140+
141+
TEST_F (BiquadCascadeFilterTests, DynamicSectionResize)
142+
{
143+
// Start with 1 section
144+
cascadeFloat.setNumSections (1);
145+
EXPECT_EQ (cascadeFloat.getNumSections(), 1u);
146+
147+
// Expand to 4 sections
148+
cascadeFloat.setNumSections (4);
149+
EXPECT_EQ (cascadeFloat.getNumSections(), 4u);
150+
151+
// Shrink to 2 sections
152+
cascadeFloat.setNumSections (2);
153+
EXPECT_EQ (cascadeFloat.getNumSections(), 2u);
154+
155+
// Should still process correctly after resize
156+
cascadeFloat.processBlock (testData.data(), outputData.data(), blockSize);
157+
158+
for (int i = 0; i < blockSize; ++i)
159+
{
160+
EXPECT_TRUE (std::isfinite (outputData[i]));
161+
}
162+
}
163+
126164
TEST_F (BiquadCascadeFilterTests, ProcessesFloatSamples)
127165
{
128166
// Set up lowpass filter on first section
129-
BiquadCascade<float> defaultCascade;
130167
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
131-
defaultCascade.setSectionCoefficients (0, coeffs);
168+
cascadeFloat.setSectionCoefficients (0, coeffs);
132169

133-
defaultCascade.processBlock (testData.data(), outputData.data(), blockSize);
170+
cascadeFloat.processBlock (testData.data(), outputData.data(), blockSize);
134171

135172
// Output should be different from input (filtered)
136173
bool outputDiffers = false;
@@ -154,11 +191,10 @@ TEST_F (BiquadCascadeFilterTests, ProcessesFloatSamples)
154191
TEST_F (BiquadCascadeFilterTests, ProcessesDoubleSamples)
155192
{
156193
// Set up lowpass filter on first section
157-
BiquadCascade<double> defaultCascade;
158194
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
159-
defaultCascade.setSectionCoefficients (0, coeffs);
195+
cascadeDouble.setSectionCoefficients (0, coeffs);
160196

161-
defaultCascade.processBlock (doubleTestData.data(), doubleOutputData.data(), blockSize);
197+
cascadeDouble.processBlock (doubleTestData.data(), doubleOutputData.data(), blockSize);
162198

163199
// Output should be different from input (filtered)
164200
bool outputDiffers = false;
@@ -212,17 +248,51 @@ TEST_F (BiquadCascadeFilterTests, MultipleSectionsCascadeCorrectly)
212248
EXPECT_LT (cascadeEnergy, singleEnergy);
213249
}
214250

251+
TEST_F (BiquadCascadeFilterTests, ProcessingThroughCascade)
252+
{
253+
cascadeFloat.setNumSections (3);
254+
255+
// Set up a multi-stage filter
256+
auto lowpass = FilterDesigner<double>::designRbjLowpass (2000.0, 0.707, sampleRate);
257+
auto peak = FilterDesigner<double>::designRbjPeak (1000.0, 2.0, 6.0, sampleRate);
258+
auto highpass = FilterDesigner<double>::designRbjHighpass (500.0, 0.707, sampleRate);
259+
260+
cascadeFloat.setSectionCoefficients (0, lowpass);
261+
cascadeFloat.setSectionCoefficients (1, peak);
262+
cascadeFloat.setSectionCoefficients (2, highpass);
263+
264+
cascadeFloat.processBlock (testData.data(), outputData.data(), blockSize);
265+
266+
for (int i = 0; i < blockSize; ++i)
267+
{
268+
EXPECT_TRUE (std::isfinite (outputData[i]));
269+
}
270+
}
271+
272+
TEST_F (BiquadCascadeFilterTests, EmptyCascade)
273+
{
274+
cascadeFloat.setNumSections (0);
275+
EXPECT_EQ (cascadeFloat.getNumSections(), 0u);
276+
277+
// Processing through empty cascade should pass signal through unchanged
278+
cascadeFloat.processBlock (testData.data(), outputData.data(), blockSize);
279+
280+
for (int i = 0; i < blockSize; ++i)
281+
{
282+
EXPECT_FLOAT_EQ (outputData[i], testData[i]);
283+
}
284+
}
285+
215286
TEST_F (BiquadCascadeFilterTests, InPlaceProcessing)
216287
{
217-
BiquadCascade<float> defaultCascade;
218288
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
219-
defaultCascade.setSectionCoefficients (0, coeffs);
289+
cascadeFloat.setSectionCoefficients (0, coeffs);
220290

221291
// Make a copy for comparison
222292
std::vector<float> originalData = testData;
223293

224294
// Process in-place
225-
defaultCascade.processBlock (testData.data(), testData.data(), blockSize);
295+
cascadeFloat.processBlock (testData.data(), testData.data(), blockSize);
226296

227297
// Output should be different from original
228298
bool outputDiffers = false;
@@ -239,38 +309,78 @@ TEST_F (BiquadCascadeFilterTests, InPlaceProcessing)
239309

240310
TEST_F (BiquadCascadeFilterTests, ResetClearsState)
241311
{
242-
BiquadCascade<float> defaultCascade;
243312
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
244-
defaultCascade.setSectionCoefficients (0, coeffs);
313+
cascadeFloat.setSectionCoefficients (0, coeffs);
245314

246315
// Process some data to build up state
247-
defaultCascade.processBlock (testData.data(), outputData.data(), blockSize);
316+
cascadeFloat.processBlock (testData.data(), outputData.data(), blockSize);
248317

249318
// Reset and process impulse
250-
defaultCascade.reset();
319+
cascadeFloat.reset();
251320

252321
std::vector<float> impulse (blockSize, 0.0f);
253322
impulse[0] = 1.0f;
254323

255-
defaultCascade.processBlock (impulse.data(), outputData.data(), blockSize);
324+
cascadeFloat.processBlock (impulse.data(), outputData.data(), blockSize);
256325

257326
// First output should be b0 coefficient (impulse response)
258327
EXPECT_NEAR (coeffs.b0, outputData[0], toleranceF);
259328
}
260329

261-
TEST_F (BiquadCascadeFilterTests, DISABLED_ImpulseResponseCharacteristics)
330+
TEST_F (BiquadCascadeFilterTests, CascadeStateReset)
331+
{
332+
cascadeFloat.setNumSections (2);
333+
334+
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
335+
cascadeFloat.setSectionCoefficients (0, coeffs);
336+
cascadeFloat.setSectionCoefficients (1, coeffs);
337+
338+
// Build up internal state
339+
for (int i = 0; i < 50; ++i)
340+
cascadeFloat.processSample (1.0f);
341+
342+
auto outputBeforeReset = cascadeFloat.processSample (0.0f);
343+
344+
cascadeFloat.reset();
345+
auto outputAfterReset = cascadeFloat.processSample (0.0f);
346+
347+
// After reset, the output should be closer to zero
348+
EXPECT_LT (std::abs (outputAfterReset), std::abs (outputBeforeReset));
349+
}
350+
351+
TEST_F (BiquadCascadeFilterTests, CascadeFrequencyResponse)
352+
{
353+
cascadeFloat.setNumSections (2);
354+
355+
// Two identical lowpass filters in cascade
356+
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
357+
cascadeFloat.setSectionCoefficients (0, coeffs);
358+
cascadeFloat.setSectionCoefficients (1, coeffs);
359+
360+
// Overall response should be the product of individual responses
361+
auto singleResponse = std::abs (BiquadFloat (BiquadFloat::Topology::directFormII).getComplexResponse (1000.0));
362+
BiquadFloat singleFilter;
363+
singleFilter.setCoefficients (coeffs);
364+
singleResponse = std::abs (singleFilter.getComplexResponse (1000.0));
365+
366+
auto cascadeResponse = std::abs (cascadeFloat.getComplexResponse (1000.0));
367+
auto expectedResponse = singleResponse * singleResponse;
368+
369+
EXPECT_NEAR (cascadeResponse, expectedResponse, 0.1f);
370+
}
371+
372+
TEST_F (BiquadCascadeFilterTests, ImpulseResponseCharacteristics)
262373
{
263374
// Set up lowpass filter
264-
BiquadCascade<float> defaultCascade;
265375
auto coeffs = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
266-
defaultCascade.setSectionCoefficients (0, coeffs);
376+
cascadeFloat.setSectionCoefficients (0, coeffs);
267377

268378
// Create impulse
269379
std::vector<float> impulse (blockSize, 0.0f);
270380
impulse[0] = 1.0f;
271381

272-
defaultCascade.reset();
273-
defaultCascade.processBlock (impulse.data(), outputData.data(), blockSize);
382+
cascadeFloat.reset();
383+
cascadeFloat.processBlock (impulse.data(), outputData.data(), blockSize);
274384

275385
// Impulse response should start with b0 and decay
276386
EXPECT_NEAR (coeffs.b0, outputData[0], toleranceF);
@@ -282,7 +392,7 @@ TEST_F (BiquadCascadeFilterTests, DISABLED_ImpulseResponseCharacteristics)
282392
}
283393
}
284394

285-
TEST_F (BiquadCascadeFilterTests, DISABLED_StabilityCheck)
395+
TEST_F (BiquadCascadeFilterTests, StabilityCheck)
286396
{
287397
// Create a high-Q filter that could become unstable
288398
auto coeffs = FilterDesigner<double>::designRbjLowpass (5000.0, 50.0, sampleRate);
@@ -303,4 +413,60 @@ TEST_F (BiquadCascadeFilterTests, DISABLED_StabilityCheck)
303413
}
304414
}
305415

306-
#endif
416+
TEST_F (BiquadCascadeFilterTests, CascadeVsManualChaining)
417+
{
418+
// Compare cascade processing with manual chaining of individual biquads
419+
auto coeffs1 = FilterDesigner<double>::designRbjLowpass (1000.0, 0.707, sampleRate);
420+
auto coeffs2 = FilterDesigner<double>::designRbjHighpass (500.0, 0.707, sampleRate);
421+
422+
// Set up cascade
423+
cascadeFloat.setNumSections (2);
424+
cascadeFloat.setSectionCoefficients (0, coeffs1);
425+
cascadeFloat.setSectionCoefficients (1, coeffs2);
426+
427+
// Set up manual chain
428+
BiquadFloat filter1, filter2;
429+
filter1.prepare (sampleRate, blockSize);
430+
filter2.prepare (sampleRate, blockSize);
431+
filter1.setCoefficients (coeffs1);
432+
filter2.setCoefficients (coeffs2);
433+
434+
std::vector<float> cascadeOutput (blockSize);
435+
std::vector<float> manualOutput (blockSize);
436+
std::vector<float> tempOutput (blockSize);
437+
438+
// Process through cascade
439+
cascadeFloat.processBlock (testData.data(), cascadeOutput.data(), blockSize);
440+
441+
// Process through manual chain
442+
filter1.processBlock (testData.data(), tempOutput.data(), blockSize);
443+
filter2.processBlock (tempOutput.data(), manualOutput.data(), blockSize);
444+
445+
// Results should be identical
446+
for (int i = 0; i < blockSize; ++i)
447+
{
448+
EXPECT_NEAR (cascadeOutput[i], manualOutput[i], toleranceF);
449+
}
450+
}
451+
452+
TEST_F (BiquadCascadeFilterTests, LargeCascade)
453+
{
454+
// Test with many sections
455+
const int numSections = 10;
456+
cascadeFloat.setNumSections (numSections);
457+
EXPECT_EQ (cascadeFloat.getNumSections(), static_cast<size_t> (numSections));
458+
459+
// Set mild filtering on each section
460+
auto coeffs = FilterDesigner<double>::designRbjLowpass (5000.0, 0.707, sampleRate);
461+
for (int i = 0; i < numSections; ++i)
462+
{
463+
cascadeFloat.setSectionCoefficients (static_cast<size_t> (i), coeffs);
464+
}
465+
466+
cascadeFloat.processBlock (testData.data(), outputData.data(), blockSize);
467+
468+
for (int i = 0; i < blockSize; ++i)
469+
{
470+
EXPECT_TRUE (std::isfinite (outputData[i]));
471+
}
472+
}

0 commit comments

Comments
 (0)