Skip to content

Commit fb53107

Browse files
test(cmocka, dcblock): add unit tests for DC blocking filter
Signed-off-by: Piotr Hoppe <piotr.hoppe@intel.com>
1 parent 3347ddc commit fb53107

5 files changed

Lines changed: 953 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
CONFIG_METEORLAKE=y
22
CONFIG_COMP_DRC=y
3+
CONFIG_COMP_DCBLOCK=y

test/cmocka/src/audio/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ endif()
2525
if(CONFIG_COMP_DRC)
2626
add_subdirectory(drc)
2727
endif()
28+
if(CONFIG_COMP_DCBLOCK)
29+
add_subdirectory(dcblock)
30+
endif()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
3+
cmocka_test(dcblock_math_test
4+
dcblock_math_test.c
5+
${PROJECT_SOURCE_DIR}/src/audio/dcblock/dcblock_generic.c
6+
${PROJECT_SOURCE_DIR}/src/math/numbers.c
7+
)
8+
9+
target_include_directories(dcblock_math_test PRIVATE ${PROJECT_SOURCE_DIR}/src/audio)
10+
target_include_directories(dcblock_math_test PRIVATE ${PROJECT_SOURCE_DIR}/src/audio/dcblock)
11+
12+
cmocka_test(dcblock_process
13+
dcblock_process.c
14+
)
15+
16+
target_include_directories(dcblock_process PRIVATE ${PROJECT_SOURCE_DIR}/src/audio)
17+
target_include_directories(dcblock_process PRIVATE ${PROJECT_SOURCE_DIR}/src/audio/dcblock)
18+
19+
# make small version of libaudio so we don't have to care
20+
# about unused missing references
21+
22+
add_compile_options(-DUNIT_TEST)
23+
24+
add_library(audio_for_dcblock STATIC
25+
${PROJECT_SOURCE_DIR}/src/audio/dcblock/dcblock.c
26+
${PROJECT_SOURCE_DIR}/src/audio/dcblock/dcblock_generic.c
27+
${PROJECT_SOURCE_DIR}/src/audio/dcblock/dcblock_ipc3.c
28+
${PROJECT_SOURCE_DIR}/src/math/numbers.c
29+
${PROJECT_SOURCE_DIR}/src/audio/module_adapter/module_adapter.c
30+
${PROJECT_SOURCE_DIR}/src/audio/module_adapter/module_adapter_ipc3.c
31+
${PROJECT_SOURCE_DIR}/src/audio/module_adapter/module/generic.c
32+
${PROJECT_SOURCE_DIR}/src/audio/buffers/comp_buffer.c
33+
${PROJECT_SOURCE_DIR}/src/audio/buffers/audio_buffer.c
34+
${PROJECT_SOURCE_DIR}/src/audio/source_api_helper.c
35+
${PROJECT_SOURCE_DIR}/src/audio/sink_api_helper.c
36+
${PROJECT_SOURCE_DIR}/src/audio/sink_source_utils.c
37+
${PROJECT_SOURCE_DIR}/src/audio/audio_stream.c
38+
${PROJECT_SOURCE_DIR}/src/audio/component.c
39+
${PROJECT_SOURCE_DIR}/src/audio/data_blob.c
40+
${PROJECT_SOURCE_DIR}/src/module/audio/source_api.c
41+
${PROJECT_SOURCE_DIR}/src/module/audio/sink_api.c
42+
${PROJECT_SOURCE_DIR}/src/ipc/ipc3/helper.c
43+
${PROJECT_SOURCE_DIR}/src/ipc/ipc-common.c
44+
${PROJECT_SOURCE_DIR}/src/ipc/ipc-helper.c
45+
${PROJECT_SOURCE_DIR}/src/lib/objpool.c
46+
${PROJECT_SOURCE_DIR}/test/cmocka/src/notifier_mocks.c
47+
${PROJECT_SOURCE_DIR}/src/audio/pipeline/pipeline-graph.c
48+
${PROJECT_SOURCE_DIR}/src/audio/pipeline/pipeline-params.c
49+
${PROJECT_SOURCE_DIR}/src/audio/pipeline/pipeline-schedule.c
50+
${PROJECT_SOURCE_DIR}/src/audio/pipeline/pipeline-stream.c
51+
${PROJECT_SOURCE_DIR}/src/audio/pipeline/pipeline-xrun.c
52+
)
53+
sof_append_relative_path_definitions(audio_for_dcblock)
54+
55+
target_link_libraries(audio_for_dcblock PRIVATE sof_options)
56+
57+
target_link_libraries(dcblock_process PRIVATE audio_for_dcblock)
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
//
3+
// Copyright(c) 2026 Intel Corporation.
4+
5+
#include <math.h>
6+
#include <stdarg.h>
7+
#include <stddef.h>
8+
#include <setjmp.h>
9+
#include <stdint.h>
10+
#include <stdio.h>
11+
#include <string.h>
12+
#include <cmocka.h>
13+
14+
#include <sof/audio/audio_stream.h>
15+
#include <sof/audio/format.h>
16+
17+
#include "dcblock.h"
18+
19+
#define TEST_CHANNELS 2
20+
#define TEST_FRAMES 256
21+
22+
/* Q2.30 coefficient close to 1.0 used for the DC removal test cases. */
23+
#define R_COEF_NEAR_ONE 1063004406 /* ~0.99 in Q2.30 */
24+
25+
/*
26+
* Reference DC blocking filter implemented in floating point. Mirrors the
27+
* fixed point recurrence y[n] = x[n] - x[n-1] + R * y[n-1] where R is the
28+
* Q2.30 coefficient converted to a real number.
29+
*/
30+
struct ref_state {
31+
double x_prev;
32+
double y_prev;
33+
};
34+
35+
static double dcblock_ref(struct ref_state *s, double r, double x)
36+
{
37+
double y = x - s->x_prev + r * s->y_prev;
38+
39+
s->x_prev = x;
40+
s->y_prev = y;
41+
return y;
42+
}
43+
44+
/* Build an audio_stream over a linear (non wrapping) sample buffer. */
45+
static void setup_stream(struct audio_stream *stream, void *data,
46+
size_t bytes, enum sof_ipc_frame fmt, int channels)
47+
{
48+
memset(stream, 0, sizeof(*stream));
49+
stream->addr = data;
50+
stream->end_addr = (char *)data + bytes;
51+
stream->r_ptr = data;
52+
stream->w_ptr = data;
53+
stream->size = bytes;
54+
stream->runtime_stream_params.frame_fmt = fmt;
55+
stream->runtime_stream_params.channels = channels;
56+
}
57+
58+
/* Fill the source buffer with a per-channel sinusoid plus a DC offset. */
59+
static void gen_input(double *ref_in, int channels, int frames, double dc)
60+
{
61+
int ch, i;
62+
63+
for (i = 0; i < frames; i++) {
64+
for (ch = 0; ch < channels; ch++) {
65+
double phase = 2.0 * M_PI * (i + 1) * (ch + 1) / 64.0;
66+
67+
ref_in[i * channels + ch] = dc + 0.3 * sin(phase);
68+
}
69+
}
70+
}
71+
72+
/*
73+
* Runs the S32 processing function and compares it to the floating point
74+
* reference. Returns the mean absolute value of the last quarter of the
75+
* output which is used to check DC convergence.
76+
*/
77+
static double run_s32_case(int32_t r_coeff, double dc, double tol_rel)
78+
{
79+
struct comp_data cd;
80+
struct audio_stream source, sink;
81+
int32_t src[TEST_FRAMES * TEST_CHANNELS];
82+
int32_t dst[TEST_FRAMES * TEST_CHANNELS];
83+
double ref_in[TEST_FRAMES * TEST_CHANNELS];
84+
struct ref_state rstate[TEST_CHANNELS];
85+
double r = (double)r_coeff / (double)ONE_Q2_30;
86+
double tail_abs_sum = 0.0;
87+
int tail_count = 0;
88+
dcblock_func func;
89+
int ch, i;
90+
91+
memset(&cd, 0, sizeof(cd));
92+
for (ch = 0; ch < TEST_CHANNELS; ch++)
93+
cd.R_coeffs[ch] = r_coeff;
94+
95+
memset(rstate, 0, sizeof(rstate));
96+
gen_input(ref_in, TEST_CHANNELS, TEST_FRAMES, dc);
97+
98+
for (i = 0; i < TEST_FRAMES * TEST_CHANNELS; i++)
99+
src[i] = (int32_t)round(ref_in[i] * 2147483647.0);
100+
101+
setup_stream(&source, src, sizeof(src), SOF_IPC_FRAME_S32_LE, TEST_CHANNELS);
102+
setup_stream(&sink, dst, sizeof(dst), SOF_IPC_FRAME_S32_LE, TEST_CHANNELS);
103+
104+
func = dcblock_find_func(SOF_IPC_FRAME_S32_LE);
105+
assert_non_null(func);
106+
assert_int_equal(func(&cd, &csrc, &csnk, TEST_FRAMES), 0);
107+
108+
for (i = 0; i < TEST_FRAMES; i++) {
109+
for (ch = 0; ch < TEST_CHANNELS; ch++) {
110+
int idx = i * TEST_CHANNELS + ch;
111+
double refy = dcblock_ref(&rstate[ch], r,
112+
(double)src[idx] / 2147483648.0);
113+
double outy = (double)dst[idx] / 2147483648.0;
114+
double delta = fabs(refy - outy);
115+
116+
if (delta > tol_rel) {
117+
printf("s32 mismatch idx %d ref %g out %g delta %g\n",
118+
idx, refy, outy, delta);
119+
assert_true(delta <= tol_rel);
120+
}
121+
122+
if (i >= TEST_FRAMES * 3 / 4) {
123+
tail_abs_sum += fabs(outy);
124+
tail_count++;
125+
}
126+
}
127+
}
128+
129+
return tail_count ? tail_abs_sum / tail_count : 0.0;
130+
}
131+
132+
/* Passthrough: R = 1.0 gives y[n] = x[n] - x[n-1], a pure differentiator. */
133+
static void test_dcblock_passthrough(void **state)
134+
{
135+
(void)state;
136+
137+
/* With no DC and R=1.0 the reference matches bit-close. */
138+
run_s32_case(ONE_Q2_30, 0.0, 1.0e-6);
139+
}
140+
141+
/* A strong DC offset must be attenuated towards zero in steady state. */
142+
static void test_dcblock_dc_removal(void **state)
143+
{
144+
(void)state;
145+
146+
double tail = run_s32_case(R_COEF_NEAR_ONE, 0.5, 1.0e-6);
147+
148+
/* The residual DC plus small sinusoid must be well below the input DC. */
149+
printf("dc_removal tail mean abs = %g\n", tail);
150+
assert_true(tail < 0.25);
151+
}
152+
153+
/* Full scale input must not overflow (saturation path in dcblock_generic). */
154+
static void test_dcblock_saturation(void **state)
155+
{
156+
(void)state;
157+
158+
struct comp_data cd;
159+
struct audio_stream source, sink;
160+
int32_t src[TEST_FRAMES * TEST_CHANNELS];
161+
int32_t dst[TEST_FRAMES * TEST_CHANNELS];
162+
dcblock_func func;
163+
int i;
164+
165+
memset(&cd, 0, sizeof(cd));
166+
for (i = 0; i < TEST_CHANNELS; i++)
167+
cd.R_coeffs[i] = ONE_Q2_30;
168+
169+
/* Alternating +full/-full scale is the worst case for the difference. */
170+
for (i = 0; i < TEST_FRAMES * TEST_CHANNELS; i++)
171+
src[i] = (i & 1) ? INT32_MAX : INT32_MIN;
172+
173+
setup_stream(&source, src, sizeof(src), SOF_IPC_FRAME_S32_LE, TEST_CHANNELS);
174+
setup_stream(&sink, dst, sizeof(dst), SOF_IPC_FRAME_S32_LE, TEST_CHANNELS);
175+
176+
func = dcblock_find_func(SOF_IPC_FRAME_S32_LE);
177+
assert_non_null(func);
178+
assert_int_equal(func(&cd, &csrc, &csnk, TEST_FRAMES), 0);
179+
180+
for (i = 0; i < TEST_FRAMES * TEST_CHANNELS; i++) {
181+
assert_true(dst[i] <= INT32_MAX);
182+
assert_true(dst[i] >= INT32_MIN);
183+
}
184+
}
185+
186+
/* Bit-exactness check against the floating point reference for S32. */
187+
static void test_dcblock_bitexact_s32(void **state)
188+
{
189+
(void)state;
190+
191+
run_s32_case(R_COEF_NEAR_ONE, 0.1, 1.0e-6);
192+
}
193+
194+
/*
195+
* Runs the S16 processing function and compares it to the floating point
196+
* reference. The component internally works in Q1.31, so the tolerance must
197+
* account for the 16-bit output quantization step (2 LSB of S16).
198+
*/
199+
static void run_s16_case(int32_t r_coeff, double dc)
200+
{
201+
struct comp_data cd;
202+
struct audio_stream source, sink;
203+
int16_t src[TEST_FRAMES * TEST_CHANNELS];
204+
int16_t dst[TEST_FRAMES * TEST_CHANNELS];
205+
double ref_in[TEST_FRAMES * TEST_CHANNELS];
206+
struct ref_state rstate[TEST_CHANNELS];
207+
double r = (double)r_coeff / (double)ONE_Q2_30;
208+
double tol = 2.0 / 32768.0;
209+
dcblock_func func;
210+
int ch, i;
211+
212+
memset(&cd, 0, sizeof(cd));
213+
for (ch = 0; ch < TEST_CHANNELS; ch++)
214+
cd.R_coeffs[ch] = r_coeff;
215+
216+
memset(rstate, 0, sizeof(rstate));
217+
gen_input(ref_in, TEST_CHANNELS, TEST_FRAMES, dc);
218+
219+
for (i = 0; i < TEST_FRAMES * TEST_CHANNELS; i++)
220+
src[i] = (int16_t)round(ref_in[i] * 32767.0);
221+
222+
setup_stream(&source, src, sizeof(src), SOF_IPC_FRAME_S16_LE, TEST_CHANNELS);
223+
setup_stream(&sink, dst, sizeof(dst), SOF_IPC_FRAME_S16_LE, TEST_CHANNELS);
224+
225+
func = dcblock_find_func(SOF_IPC_FRAME_S16_LE);
226+
assert_non_null(func);
227+
assert_int_equal(func(&cd, &csrc, &csnk, TEST_FRAMES), 0);
228+
229+
for (i = 0; i < TEST_FRAMES; i++) {
230+
for (ch = 0; ch < TEST_CHANNELS; ch++) {
231+
int idx = i * TEST_CHANNELS + ch;
232+
double refy = dcblock_ref(&rstate[ch], r,
233+
(double)src[idx] / 32768.0);
234+
double outy = (double)dst[idx] / 32768.0;
235+
double delta = fabs(refy - outy);
236+
237+
if (delta > tol) {
238+
printf("s16 mismatch idx %d ref %g out %g delta %g\n",
239+
idx, refy, outy, delta);
240+
assert_true(delta <= tol);
241+
}
242+
}
243+
}
244+
}
245+
246+
/* Bit-exactness check against the floating point reference for S16. */
247+
static void test_dcblock_bitexact_s16(void **state)
248+
{
249+
(void)state;
250+
251+
run_s16_case(R_COEF_NEAR_ONE, 0.1);
252+
}
253+
254+
/*
255+
* Runs the S24 (in 32-bit container) processing function and compares it to
256+
* the floating point reference with a tolerance of 2 LSB of S24.
257+
*/
258+
static void run_s24_case(int32_t r_coeff, double dc)
259+
{
260+
struct comp_data cd;
261+
struct audio_stream source, sink;
262+
int32_t src[TEST_FRAMES * TEST_CHANNELS];
263+
int32_t dst[TEST_FRAMES * TEST_CHANNELS];
264+
double ref_in[TEST_FRAMES * TEST_CHANNELS];
265+
struct ref_state rstate[TEST_CHANNELS];
266+
double r = (double)r_coeff / (double)ONE_Q2_30;
267+
double tol = 2.0 / 8388608.0;
268+
dcblock_func func;
269+
int ch, i;
270+
271+
memset(&cd, 0, sizeof(cd));
272+
for (ch = 0; ch < TEST_CHANNELS; ch++)
273+
cd.R_coeffs[ch] = r_coeff;
274+
275+
memset(rstate, 0, sizeof(rstate));
276+
gen_input(ref_in, TEST_CHANNELS, TEST_FRAMES, dc);
277+
278+
for (i = 0; i < TEST_FRAMES * TEST_CHANNELS; i++)
279+
src[i] = (int32_t)round(ref_in[i] * 8388607.0);
280+
281+
setup_stream(&source, src, sizeof(src), SOF_IPC_FRAME_S24_4LE, TEST_CHANNELS);
282+
setup_stream(&sink, dst, sizeof(dst), SOF_IPC_FRAME_S24_4LE, TEST_CHANNELS);
283+
284+
func = dcblock_find_func(SOF_IPC_FRAME_S24_4LE);
285+
assert_non_null(func);
286+
func(&cd, &source, &sink, TEST_FRAMES);
287+
288+
for (i = 0; i < TEST_FRAMES; i++) {
289+
for (ch = 0; ch < TEST_CHANNELS; ch++) {
290+
int idx = i * TEST_CHANNELS + ch;
291+
double refy = dcblock_ref(&rstate[ch], r,
292+
(double)src[idx] / 8388608.0);
293+
double outy = (double)dst[idx] / 8388608.0;
294+
double delta = fabs(refy - outy);
295+
296+
if (delta > tol) {
297+
printf("s24 mismatch idx %d ref %g out %g delta %g\n",
298+
idx, refy, outy, delta);
299+
assert_true(delta <= tol);
300+
}
301+
}
302+
}
303+
}
304+
305+
/* Bit-exactness check against the floating point reference for S24. */
306+
static void test_dcblock_bitexact_s24(void **state)
307+
{
308+
(void)state;
309+
310+
run_s24_case(R_COEF_NEAR_ONE, 0.1);
311+
}
312+
313+
int main(void)
314+
{
315+
const struct CMUnitTest tests[] = {
316+
cmocka_unit_test(test_dcblock_passthrough),
317+
cmocka_unit_test(test_dcblock_dc_removal),
318+
cmocka_unit_test(test_dcblock_saturation),
319+
cmocka_unit_test(test_dcblock_bitexact_s32),
320+
cmocka_unit_test(test_dcblock_bitexact_s16),
321+
cmocka_unit_test(test_dcblock_bitexact_s24),
322+
};
323+
324+
cmocka_set_message_output(CM_OUTPUT_TAP);
325+
326+
return cmocka_run_group_tests(tests, NULL, NULL);
327+
}

0 commit comments

Comments
 (0)