diff --git a/src/arch/xtensa/configs/unit_test_defconfig b/src/arch/xtensa/configs/unit_test_defconfig index afe031acedba..b8b193cb137f 100644 --- a/src/arch/xtensa/configs/unit_test_defconfig +++ b/src/arch/xtensa/configs/unit_test_defconfig @@ -1,2 +1,4 @@ CONFIG_METEORLAKE=y CONFIG_COMP_DRC=y +CONFIG_COMP_CROSSOVER=y +CONFIG_MATH_IIR_DF2T=y diff --git a/src/audio/crossover/crossover.c b/src/audio/crossover/crossover.c index cbc45a04ec50..4bcd32fbbb47 100644 --- a/src/audio/crossover/crossover.c +++ b/src/audio/crossover/crossover.c @@ -5,11 +5,14 @@ // Author: Sebastiano Carlucci #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -84,14 +87,13 @@ int crossover_get_stream_index(struct processing_module *mod, * Refer to sof/src/include/sof/crossover.h for more information on assigning * sinks to an output. * - * \param[out] sinks array where the sinks are assigned + * \param[out] assigned_sinks array where the sinks are assigned, indexed by + * the configured band (stream) index * \return number of sinks assigned. This number should be equal to * config->num_sinks if no errors were found. */ static int crossover_assign_sinks(struct processing_module *mod, - struct output_stream_buffer *output_buffers, - struct output_stream_buffer **assigned_obufs, - bool *enabled) + struct sof_sink **assigned_sinks) { struct comp_data *cd = module_get_private_data(mod); struct sof_crossover_config *config = cd->config; @@ -103,6 +105,7 @@ static int crossover_assign_sinks(struct processing_module *mod, comp_dev_for_each_consumer(dev, sink) { unsigned int sink_id, state; + struct sof_sink *snk = audio_buffer_get_sink(&sink->audio_buffer); sink_id = crossover_get_sink_id(cd, buffer_pipeline_id(sink), j); state = comp_buffer_get_sink_state(sink); @@ -113,8 +116,8 @@ static int crossover_assign_sinks(struct processing_module *mod, /* If no config is set, then assign the sinks in order */ if (!config) { - assigned_obufs[num_sinks++] = &output_buffers[j]; - enabled[j++] = true; + assigned_sinks[num_sinks++] = snk; + j++; continue; } @@ -130,15 +133,15 @@ static int crossover_assign_sinks(struct processing_module *mod, break; } - if (assigned_obufs[i]) { + if (assigned_sinks[i]) { comp_err(dev, "multiple sinks with id %d are assigned", sink_id); break; } - assigned_obufs[i] = &output_buffers[j]; - enabled[j++] = true; + assigned_sinks[i] = snk; + j++; num_sinks++; } @@ -460,29 +463,22 @@ static int crossover_get_config(struct processing_module *mod, /** * \brief Copies and processes stream data. - * \param[in,out] dev Crossover Filter base component device. + * \param[in,out] mod Crossover Filter processing module. * \return Error code. */ -static int crossover_process_audio_stream(struct processing_module *mod, - struct input_stream_buffer *input_buffers, - int num_input_buffers, - struct output_stream_buffer *output_buffers, - int num_output_buffers) +static int crossover_process_data(struct processing_module *mod, + struct sof_source **sources, + int num_of_sources, + struct sof_sink **sinks, + int num_of_sinks) { - struct output_stream_buffer *assigned_obufs[SOF_CROSSOVER_MAX_STREAMS] = { NULL }; - bool enabled_buffers[PLATFORM_MAX_STREAMS] = { false }; + struct sof_sink *assigned_sinks[SOF_CROSSOVER_MAX_STREAMS] = { NULL }; struct comp_data *cd = module_get_private_data(mod); struct comp_dev *dev = mod->dev; - struct audio_stream *source = input_buffers[0].data; + struct sof_source *source = sources[0]; uint32_t num_sinks; uint32_t num_assigned_sinks = 0; - /* The frames count to process from module adapter applies for source buffer and - * all sink buffers. The function module_single_source_setup() checks the frames - * avail/free from all source and sink combinations. - */ - uint32_t frames = input_buffers[0].size; - uint32_t frame_bytes = audio_stream_frame_bytes(input_buffers[0].data); - uint32_t processed_bytes; + uint32_t frames; struct sof_crossover_config *prev_config; uint32_t prev_num_sinks; size_t cfg_size; @@ -513,7 +509,7 @@ static int crossover_process_audio_stream(struct processing_module *mod, return -EINVAL; } - ret = crossover_setup(mod, audio_stream_get_channels(source)); + ret = crossover_setup(mod, source_get_channels(source)); if (ret < 0) { comp_err(dev, "failed Crossover setup"); return ret; @@ -524,12 +520,11 @@ static int crossover_process_audio_stream(struct processing_module *mod, * the output to the corresponding sinks. * It is possible for an assigned sink to be in a different * state than the component. Therefore not all sinks are guaranteed - * to be assigned: sink[i] can be NULL, 0 <= i <= config->num_sinks + * to be assigned: assigned_sinks[i] can be NULL, 0 <= i < config->num_sinks */ - num_assigned_sinks = crossover_assign_sinks(mod, output_buffers, assigned_obufs, - enabled_buffers); + num_assigned_sinks = crossover_assign_sinks(mod, assigned_sinks); if (cd->config && num_assigned_sinks != cd->config->num_sinks) - comp_dbg(dev, "crossover_copy(), number of assigned sinks (%i) does not match number of sinks in config (%i).", + comp_dbg(dev, "crossover_process(), number of assigned sinks (%i) does not match number of sinks in config (%i).", num_assigned_sinks, cd->config->num_sinks); /* If no config is set then assign the number of sinks to the number @@ -540,20 +535,21 @@ static int crossover_process_audio_stream(struct processing_module *mod, else num_sinks = num_assigned_sinks; + /* The number of frames to process is bound by the source data available + * and the free space in every assigned sink. + */ + frames = source_get_data_frames_available(source); + for (i = 0; i < num_sinks; i++) { + if (assigned_sinks[i]) + frames = MIN(frames, sink_get_free_frames(assigned_sinks[i])); + } + frames = MIN(frames, dev->frames); + /* Process crossover */ if (!frames) - return -ENODATA; - - cd->crossover_process(cd, input_buffers, assigned_obufs, num_sinks, frames); + return 0; - processed_bytes = frames * frame_bytes; - mod->input_buffers[0].consumed = processed_bytes; - for (i = 0; i < num_output_buffers; i++) { - if (enabled_buffers[i]) - mod->output_buffers[i].size = processed_bytes; - } - - return 0; + return cd->crossover_process(cd, source, assigned_sinks, num_sinks, frames); } /** @@ -668,7 +664,7 @@ static int crossover_reset(struct processing_module *mod) static const struct module_interface crossover_interface = { .init = crossover_init, .prepare = crossover_prepare, - .process_audio_stream = crossover_process_audio_stream, + .process = crossover_process_data, .set_configuration = crossover_set_config, .get_configuration = crossover_get_config, .reset = crossover_reset, diff --git a/src/audio/crossover/crossover.h b/src/audio/crossover/crossover.h index 2312a1d53857..42f27574280b 100644 --- a/src/audio/crossover/crossover.h +++ b/src/audio/crossover/crossover.h @@ -19,6 +19,8 @@ struct comp_buffer; struct comp_dev; +struct sof_source; +struct sof_sink; /** * The Crossover filter will have from 2 to 4 outputs. @@ -52,11 +54,11 @@ struct comp_dev; struct comp_data; -typedef void (*crossover_process)(struct comp_data *cd, - struct input_stream_buffer *bsource, - struct output_stream_buffer *bsinks[], - int32_t num_sinks, - uint32_t frames); +typedef int (*crossover_process)(struct comp_data *cd, + struct sof_source *source, + struct sof_sink **sinks, + int32_t num_sinks, + uint32_t frames); /* Crossover component private data */ struct comp_data { diff --git a/src/audio/crossover/crossover_generic.c b/src/audio/crossover/crossover_generic.c index 028018934816..a650e27a7453 100644 --- a/src/audio/crossover/crossover_generic.c +++ b/src/audio/crossover/crossover_generic.c @@ -8,7 +8,11 @@ #include #include #include +#include +#include +#include #include +#include #include #include "crossover.h" @@ -87,96 +91,164 @@ static void crossover_generic_split_4way(int32_t in, z2, &out[2], &out[3]); } -static void crossover_default_pass(struct comp_data *cd, - struct input_stream_buffer *bsource, - struct output_stream_buffer **bsinks, - int32_t num_sinks, - uint32_t frames) +/* + * \brief Passthrough copy of the source samples to every assigned sink. + * + * Used when the component runs without a configuration blob. The source + * data are copied to each active sink without being freed (free == false) + * so that the same input is replicated to all sinks; the source is + * released once after the last copy. + * + * \return 0 on success, negative error code otherwise. + */ +static int crossover_default_pass(struct comp_data *cd, + struct sof_source *source, + struct sof_sink **sinks, + int32_t num_sinks, + uint32_t frames) { - const struct audio_stream *source_stream = bsource->data; - uint32_t samples = audio_stream_get_channels(source_stream) * frames; + size_t bytes = frames * source_get_frame_bytes(source); + int ret; int i; for (i = 0; i < num_sinks; i++) { - if (!bsinks[i]) + if (!sinks[i]) continue; - audio_stream_copy(source_stream, 0, bsinks[i]->data, 0, samples); + ret = source_to_sink_copy(source, sinks[i], false, bytes); + if (ret) + return ret; } + + return source_release_data(source, bytes); } #if CONFIG_FORMAT_S16LE -static void crossover_s16_default(struct comp_data *cd, - struct input_stream_buffer *bsource, - struct output_stream_buffer **bsinks, - int32_t num_sinks, - uint32_t frames) +static int crossover_s16_default(struct comp_data *cd, + struct sof_source *source, + struct sof_sink **sinks, + int32_t num_sinks, + uint32_t frames) { + int16_t *y[SOF_CROSSOVER_MAX_STREAMS]; + int16_t *y_start[SOF_CROSSOVER_MAX_STREAMS]; + int16_t *y_end[SOF_CROSSOVER_MAX_STREAMS]; + int out_idx[SOF_CROSSOVER_MAX_STREAMS]; + int32_t out[SOF_CROSSOVER_MAX_STREAMS]; struct crossover_state *state; - const struct audio_stream *source_stream = bsource->data; - struct audio_stream *sink_stream; - int16_t *x, *y; + int16_t const *x, *x_start, *x_end; + int x_samples, y_samples; + int nch = source_get_channels(source); + size_t bytes = frames * source_get_frame_bytes(source); + int active_sinks = 0; int ch, i, j; - int idx; - int nch = audio_stream_get_channels(source_stream); - int32_t out[num_sinks]; - - for (ch = 0; ch < nch; ch++) { - idx = ch; - state = &cd->state[ch]; - for (i = 0; i < frames; i++) { - x = audio_stream_read_frag_s16(source_stream, idx); - cd->crossover_split(*x << 16, out, state); + int ret; - for (j = 0; j < num_sinks; j++) { - if (!bsinks[j]) - continue; - sink_stream = bsinks[j]->data; - y = audio_stream_write_frag_s16(sink_stream, - idx); - *y = sat_int16(Q_SHIFT_RND(out[j], 31, 15)); - } + ret = source_get_data_s16(source, bytes, &x, &x_start, &x_samples); + if (ret) + return ret; + x_end = x_start + x_samples; - idx += nch; + for (j = 0; j < num_sinks; j++) { + if (!sinks[j]) + continue; + ret = sink_get_buffer_s16(sinks[j], bytes, &y[active_sinks], + &y_start[active_sinks], &y_samples); + if (ret) + return ret; + y_end[active_sinks] = y_start[active_sinks] + y_samples; + out_idx[active_sinks] = j; + active_sinks++; + } + + for (i = 0; i < frames; i++) { + for (ch = 0; ch < nch; ch++) { + state = &cd->state[ch]; + cd->crossover_split(*x << 16, out, state); + if (++x >= x_end) + x = x_start; + for (j = 0; j < active_sinks; j++) { + *y[j] = sat_int16(Q_SHIFT_RND(out[out_idx[j]], 31, 15)); + if (++y[j] >= y_end[j]) + y[j] = y_start[j]; + } } } + + ret = source_release_data(source, bytes); + if (ret) + return ret; + for (j = 0; j < active_sinks; j++) { + ret = sink_commit_buffer(sinks[out_idx[j]], bytes); + if (ret) + return ret; + } + + return 0; } #endif /* CONFIG_FORMAT_S16LE */ #if CONFIG_FORMAT_S24LE -static void crossover_s24_default(struct comp_data *cd, - struct input_stream_buffer *bsource, - struct output_stream_buffer **bsinks, - int32_t num_sinks, - uint32_t frames) +static int crossover_s24_default(struct comp_data *cd, + struct sof_source *source, + struct sof_sink **sinks, + int32_t num_sinks, + uint32_t frames) { + int32_t *y[SOF_CROSSOVER_MAX_STREAMS]; + int32_t *y_start[SOF_CROSSOVER_MAX_STREAMS]; + int32_t *y_end[SOF_CROSSOVER_MAX_STREAMS]; + int out_idx[SOF_CROSSOVER_MAX_STREAMS]; + int32_t out[SOF_CROSSOVER_MAX_STREAMS]; struct crossover_state *state; - const struct audio_stream *source_stream = bsource->data; - struct audio_stream *sink_stream; - int32_t *x, *y; + int32_t const *x, *x_start, *x_end; + int x_samples, y_samples; + int nch = source_get_channels(source); + size_t bytes = frames * source_get_frame_bytes(source); + int active_sinks = 0; int ch, i, j; - int idx; - int nch = audio_stream_get_channels(source_stream); - int32_t out[num_sinks]; - - for (ch = 0; ch < nch; ch++) { - idx = ch; - state = &cd->state[ch]; - for (i = 0; i < frames; i++) { - x = audio_stream_read_frag_s32(source_stream, idx); - cd->crossover_split(*x << 8, out, state); + int ret; - for (j = 0; j < num_sinks; j++) { - if (!bsinks[j]) - continue; - sink_stream = bsinks[j]->data; - y = audio_stream_write_frag_s32(sink_stream, - idx); - *y = sat_int24(Q_SHIFT_RND(out[j], 31, 23)); - } + ret = source_get_data_s32(source, bytes, &x, &x_start, &x_samples); + if (ret) + return ret; + x_end = x_start + x_samples; + + for (j = 0; j < num_sinks; j++) { + if (!sinks[j]) + continue; + ret = sink_get_buffer_s32(sinks[j], bytes, &y[active_sinks], + &y_start[active_sinks], &y_samples); + if (ret) + return ret; + y_end[active_sinks] = y_start[active_sinks] + y_samples; + out_idx[active_sinks] = j; + active_sinks++; + } - idx += nch; + for (i = 0; i < frames; i++) { + for (ch = 0; ch < nch; ch++) { + state = &cd->state[ch]; + cd->crossover_split(*x << 8, out, state); + if (++x >= x_end) + x = x_start; + for (j = 0; j < active_sinks; j++) { + *y[j] = sat_int24(Q_SHIFT_RND(out[out_idx[j]], 31, 23)); + if (++y[j] >= y_end[j]) + y[j] = y_start[j]; + } } } + + ret = source_release_data(source, bytes); + if (ret) + return ret; + for (j = 0; j < active_sinks; j++) { + ret = sink_commit_buffer(sinks[out_idx[j]], bytes); + if (ret) + return ret; + } + + return 0; } #endif /* CONFIG_FORMAT_S24LE */ @@ -184,68 +256,80 @@ static void crossover_s24_default(struct comp_data *cd, /** * \brief Processes audio frames with a crossover filter for s32 format. * - * This function divides audio data from an input stream into multiple output - * streams based on a crossover filter. It reads the input audio data, applies - * the crossover filter, and writes the processed audio data to active output - * streams. + * This function reads the interleaved audio data from the source, applies + * the crossover split for each channel, and writes the per-band results to + * the assigned sinks using the sink circular buffer API. * * \param cd Pointer to the component data structure which holds the crossover state. - * \param bsource Pointer to the input stream buffer structure. - * \param bsinks Array of pointers to output stream buffer structures. - * \param num_sinks Number of output stream buffers in the bsinks array. + * \param source Source handle to read audio data from. + * \param sinks Array of sink handles, indexed by band; entries may be NULL. + * \param num_sinks Number of entries in the sinks array. * \param frames Number of audio frames to process. + * \return 0 on success, negative error code otherwise. */ -static void crossover_s32_default(struct comp_data *cd, - struct input_stream_buffer *bsource, - struct output_stream_buffer **bsinks, - int32_t num_sinks, - uint32_t frames) +static int crossover_s32_default(struct comp_data *cd, + struct sof_source *source, + struct sof_sink **sinks, + int32_t num_sinks, + uint32_t frames) { - /* Array to hold active sink streams; initialized to null */ - struct audio_stream *sink_stream[SOF_CROSSOVER_MAX_STREAMS] = { NULL }; + int32_t *y[SOF_CROSSOVER_MAX_STREAMS]; + int32_t *y_start[SOF_CROSSOVER_MAX_STREAMS]; + int32_t *y_end[SOF_CROSSOVER_MAX_STREAMS]; + int out_idx[SOF_CROSSOVER_MAX_STREAMS]; + int32_t out[SOF_CROSSOVER_MAX_STREAMS]; struct crossover_state *state; - /* Source stream to read audio data from */ - const struct audio_stream *source_stream = bsource->data; - int32_t *x, *y; - int ch, i, j; - int idx; - /* Counter for active sink streams */ + int32_t const *x, *x_start, *x_end; + int x_samples, y_samples; + int nch = source_get_channels(source); + size_t bytes = frames * source_get_frame_bytes(source); int active_sinks = 0; - /* Number of channels in the source stream */ - int nch = audio_stream_get_channels(source_stream); - /* Output buffer for processed data */ - int32_t out[num_sinks]; + int ch, i, j; + int ret; + + ret = source_get_data_s32(source, bytes, &x, &x_start, &x_samples); + if (ret) + return ret; + x_end = x_start + x_samples; - /* Identify active sinks, avoid processing null sinks later */ + /* Identify active sinks, keeping the band index for correct routing */ for (j = 0; j < num_sinks; j++) { - if (bsinks[j]) - sink_stream[active_sinks++] = bsinks[j]->data; + if (!sinks[j]) + continue; + ret = sink_get_buffer_s32(sinks[j], bytes, &y[active_sinks], + &y_start[active_sinks], &y_samples); + if (ret) + return ret; + y_end[active_sinks] = y_start[active_sinks] + y_samples; + out_idx[active_sinks] = j; + active_sinks++; } - /* Process for each channel */ - /* Loop through each channel in the source stream */ - for (ch = 0; ch < nch; ch++) { - /* Set current crossover state for this channel */ - state = &cd->state[ch]; - /* Iterate over frames */ - /* Loop through each frame */ - for (i = 0, idx = ch; i < frames; i++, idx += nch) { - /* Read source */ - /* Read the current audio frame for the channel */ - x = audio_stream_read_frag_s32(source_stream, idx); - /* Apply the crossover split logic to the audio data */ + /* Frame-major loop keeps sequential access into the circular buffers */ + for (i = 0; i < frames; i++) { + for (ch = 0; ch < nch; ch++) { + state = &cd->state[ch]; cd->crossover_split(*x, out, state); - - /* Write output to active sinks */ - /* Write processed output to active sinks */ + if (++x >= x_end) + x = x_start; for (j = 0; j < active_sinks; j++) { - /* Write processed data to sink */ - y = audio_stream_write_frag_s32(sink_stream[j], idx); - *y = out[j]; + *y[j] = out[out_idx[j]]; + if (++y[j] >= y_end[j]) + y[j] = y_start[j]; } - } } + + ret = source_release_data(source, bytes); + if (ret) + return ret; + for (j = 0; j < active_sinks; j++) { + ret = sink_commit_buffer(sinks[out_idx[j]], bytes); + if (ret) + return ret; + } + + return 0; } #endif /* CONFIG_FORMAT_S32LE */ diff --git a/test/cmocka/src/audio/CMakeLists.txt b/test/cmocka/src/audio/CMakeLists.txt index d199fd18a0ea..b518678631c6 100644 --- a/test/cmocka/src/audio/CMakeLists.txt +++ b/test/cmocka/src/audio/CMakeLists.txt @@ -25,3 +25,6 @@ endif() if(CONFIG_COMP_DRC) add_subdirectory(drc) endif() +if(CONFIG_COMP_CROSSOVER) + add_subdirectory(crossover) +endif() diff --git a/test/cmocka/src/audio/crossover/CMakeLists.txt b/test/cmocka/src/audio/crossover/CMakeLists.txt new file mode 100644 index 000000000000..d517ee8e4f1b --- /dev/null +++ b/test/cmocka/src/audio/crossover/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: BSD-3-Clause + +cmocka_test(crossover_lr4_test + crossover_lr4_test.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_generic.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_hifi3.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_hifi4.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_hifi5.c + ${PROJECT_SOURCE_DIR}/src/math/numbers.c +) + +target_link_libraries(crossover_lr4_test PRIVATE m) + +# crossover_split_test: tests the split_2way / split_3way / split_4way routing +# topology by compiling crossover_generic.c directly. The per-format processing +# functions in crossover_generic.c reference the source/sink API; those symbols +# are satisfied by the trivial stubs in crossover_test_mocks.c (shareable by any +# test that links crossover_generic.c). + +add_compile_options(-DUNIT_TEST) + +cmocka_test(crossover_split_test + crossover_split_test.c + crossover_test_mocks.c + ${PROJECT_SOURCE_DIR}/src/audio/crossover/crossover_generic.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_generic.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_hifi3.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_hifi4.c + ${PROJECT_SOURCE_DIR}/src/math/iir_df1_hifi5.c + ${PROJECT_SOURCE_DIR}/src/math/numbers.c + ${PROJECT_SOURCE_DIR}/src/audio/audio_stream.c +) + +target_include_directories(crossover_split_test PRIVATE + ${PROJECT_SOURCE_DIR}/src/audio/crossover +) + +target_link_libraries(crossover_split_test PRIVATE m) diff --git a/test/cmocka/src/audio/crossover/crossover_lr4_test.c b/test/cmocka/src/audio/crossover/crossover_lr4_test.c new file mode 100644 index 000000000000..f71cd8177548 --- /dev/null +++ b/test/cmocka/src/audio/crossover/crossover_lr4_test.c @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2025 Intel Corporation. All rights reserved. +// +// Author: Piotr Hoppe + +/** + * @file crossover_lr4_test.c + * @brief Unit tests for the LR4 (Linkwitz-Riley 4th order) biquad filter + * used by the Crossover component. + * + * The Crossover module decomposes each audio channel into frequency bands + * using LR4 filters — each implemented as two identical 2nd-order biquads + * cascaded in series, processed via iir_df1_4th(). + * + * These tests verify the fundamental DSP properties of the LR4 filter + * without depending on the full SOF component stack: + * + * 1. Identity biquad passes signal unchanged. + * 2. LP LR4 passes DC and blocks near-Nyquist signals. + * 3. HP LR4 blocks DC and passes near-Nyquist signals. + * 4. LP and HP split input power equally at the crossover frequency + * (defining property of Linkwitz-Riley crossovers: each band is −3 dB). + * 5. LP LR4 strongly attenuates high-frequency signals (selectivity). + * 6. HP LR4 strongly attenuates low-frequency signals (selectivity). + * 7. LP and HP biquads share the same denominator (poles are identical). + * + * Coefficient computation follows the Web Audio resonance filter design used + * in src/audio/crossover/tune/sof_crossover_gen_coefs.m. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/* ========================================================================= + * Constants + * ========================================================================= */ + +/** Sample rate used for all tests */ +#define FS 48000.0 + +/** Crossover frequency (Hz) */ +#define FC_HZ 1000.0 + +/** Total signal length (samples) */ +#define SIGNAL_LEN 512 + +/** + * Number of leading samples to discard when measuring steady-state + * response. At 1 kHz / 48 kHz the LR4 time constant is ~5 periods + * (~240 samples); 288 gives comfortable headroom. + */ +#define WARMUP_LEN 288 + +/** Q2.30: 1.0 in Q2.30 fixed-point (coefficient scale) */ +#define Q30 ((int64_t)1 << 30) + +/** Q2.14: 1.0 in Q2.14 fixed-point (iir_df1 output gain = unity) */ +#define UNITY_GAIN_Q14 ((int32_t)(1 << 14)) + +/** + * Number of int32_t words in one biquad coefficient set. + * Order: {a2, a1, b2, b1, b0, output_shift, output_gain}. + */ +#define BIQUAD_NCOEF SOF_EQ_IIR_NBIQUAD /* 7 */ + +/** LR4 = two identical biquads in series */ +#define LR4_NCOEF (2 * BIQUAD_NCOEF) + +/** 4 delay slots per biquad, 2 biquads */ +#define LR4_NDELAY (2 * IIR_DF1_NUM_STATE) + +/* ========================================================================= + * Helper: biquad coefficient computation + * ========================================================================= + * + * Computes Butterworth 2nd-order LP/HP biquad coefficients using the same + * Web Audio resonance filter design as sof_crossover_gen_coefs.m. + * + * The SOF iir_df1 engine stores a-coefficients NEGATED with respect to the + * standard transfer function convention. Standard IIR DF-I recurrence: + * + * y[n] = b0·x[n] + b1·x[n-1] + b2·x[n-2] − a1_std·y[n-1] − a2_std·y[n-2] + * + * SOF iir_df1 accumulates: + * + * acc += coef[a1] · y[n-1] + coef[a2] · y[n-2] (addition, not subtraction) + * + * Therefore: coef[a1] = −a1_std, coef[a2] = −a2_std. + * + * All b and negated-a coefficients are quantised to Q2.30. + * Output gain is Q2.14 unity (16384). Output shift is 0. + */ + +/** + * @brief Compute SOF-format biquad coefficients for a 2nd-order LP filter. + * + * @param fs Sample rate (Hz). + * @param fc Cutoff frequency (Hz). + * @param coef Output array of BIQUAD_NCOEF int32_t values. + */ +static void compute_biquad_lp(double fs, double fc, int32_t coef[BIQUAD_NCOEF]) +{ + double cutoff = fc / (fs / 2.0); + double d = M_SQRT2; /* resonance = 0 */ + double theta = M_PI * cutoff; + double sn = 0.5 * d * sin(theta); + double beta = 0.5 * (1.0 - sn) / (1.0 + sn); + double gamma_v = (0.5 + beta) * cos(theta); + double alpha = 0.25 * (0.5 + beta - gamma_v); + + double b0 = 2.0 * alpha; + double b1 = 4.0 * alpha; /* LP: positive b1 */ + double b2 = 2.0 * alpha; + double a1_std = -2.0 * gamma_v; + double a2_std = 2.0 * beta; + + coef[0] = (int32_t)llround(-a2_std * Q30); /* negated a2 */ + coef[1] = (int32_t)llround(-a1_std * Q30); /* negated a1 */ + coef[2] = (int32_t)llround(b2 * Q30); + coef[3] = (int32_t)llround(b1 * Q30); + coef[4] = (int32_t)llround(b0 * Q30); + coef[5] = 0; /* output_shift = 0 */ + coef[6] = UNITY_GAIN_Q14; /* output_gain = 1.0 */ +} + +/** + * @brief Compute SOF-format biquad coefficients for a 2nd-order HP filter. + * + * LP and HP share the same denominator (a-coefficients); only the + * b-coefficients differ. + * + * @param fs Sample rate (Hz). + * @param fc Cutoff frequency (Hz). + * @param coef Output array of BIQUAD_NCOEF int32_t values. + */ +static void compute_biquad_hp(double fs, double fc, int32_t coef[BIQUAD_NCOEF]) +{ + double cutoff = fc / (fs / 2.0); + double d = M_SQRT2; + double theta = M_PI * cutoff; + double sn = 0.5 * d * sin(theta); + double beta = 0.5 * (1.0 - sn) / (1.0 + sn); + double gamma_v = (0.5 + beta) * cos(theta); + double alpha = 0.25 * (0.5 + beta + gamma_v); /* HP: different alpha */ + + double b0 = 2.0 * alpha; + double b1 = -4.0 * alpha; /* HP: negative b1 */ + double b2 = 2.0 * alpha; + double a1_std = -2.0 * gamma_v; /* same denominator as LP */ + double a2_std = 2.0 * beta; + + coef[0] = (int32_t)llround(-a2_std * Q30); + coef[1] = (int32_t)llround(-a1_std * Q30); + coef[2] = (int32_t)llround(b2 * Q30); + coef[3] = (int32_t)llround(b1 * Q30); + coef[4] = (int32_t)llround(b0 * Q30); + coef[5] = 0; + coef[6] = UNITY_GAIN_Q14; +} + +/* ========================================================================= + * Helper: LR4 state initialisation + * ========================================================================= */ + +/** + * @brief Initialise an LR4 iir_state_df1 from a single biquad coefficient set. + * + * An LR4 filter is two identical biquads in series. The coefficient buffer + * must hold LR4_NCOEF words; the delay buffer must hold LR4_NDELAY words. + * + * @param lr4 IIR state to initialise. + * @param coef Buffer of LR4_NCOEF int32_t (storage for two biquads). + * @param delay Buffer of LR4_NDELAY int32_t (zeroed delay lines). + * @param biquad_coef Single biquad coefficient set (BIQUAD_NCOEF words). + */ +static void init_lr4(struct iir_state_df1 *lr4, + int32_t coef[LR4_NCOEF], + int32_t delay[LR4_NDELAY], + const int32_t biquad_coef[BIQUAD_NCOEF]) +{ + memcpy(coef, biquad_coef, BIQUAD_NCOEF * sizeof(int32_t)); + memcpy(coef + BIQUAD_NCOEF, biquad_coef, BIQUAD_NCOEF * sizeof(int32_t)); + memset(delay, 0, LR4_NDELAY * sizeof(int32_t)); + lr4->biquads = 2; + lr4->biquads_in_series = 2; + lr4->coef = coef; + lr4->delay = delay; +} + +/* ========================================================================= + * Helper: signal metrics + * ========================================================================= */ + +/** + * @brief Compute the RMS of an array of Q1.31 samples. + * + * Converts each sample to double in [−1, 1] before squaring to avoid + * integer overflow. + * + * @param buf Array of int32_t Q1.31 samples. + * @param n Number of samples. + * @return RMS value in [0, 1]. + */ +static double rms_q31(const int32_t *buf, int n) +{ + double sum = 0.0; + int i; + + for (i = 0; i < n; i++) { + double s = (double)buf[i] / (double)(1u << 31); + + sum += s * s; + } + return sqrt(sum / n); +} + +/* ========================================================================= + * Tests + * ========================================================================= */ + +/** + * @brief Test 1 – identity biquad passes every sample unchanged. + * + * A biquad with b0 = 1.0 and all other coefficients zero implements y[n] = x[n]. + * Two such biquads in series (LR4) must also be y[n] = x[n]. + */ +static void test_lr4_identity_passthrough(void **state) +{ + (void)state; + + struct iir_state_df1 lr4; + int32_t coef[LR4_NCOEF]; + int32_t delay[LR4_NDELAY]; + + /* + * Identity biquad: b0 = 1.0 (Q2.30), gain = 1.0 (Q2.14), + * a2 = a1 = b2 = b1 = 0, output_shift = 0. + */ + const int32_t identity_bq[BIQUAD_NCOEF] = { + 0, 0, 0, 0, (int32_t)Q30, 0, UNITY_GAIN_Q14 + }; + + init_lr4(&lr4, coef, delay, identity_bq); + + /* Feed a 100 Hz sine and verify output equals input sample-by-sample */ + for (int i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(0.5 * INT32_MAX * + sin(2.0 * M_PI * 100.0 * i / FS)); + int32_t y = iir_df1_4th(&lr4, x); + + assert_int_equal(x, y); + } +} + +/** + * @brief Test 2 – LP LR4 passes DC and blocks near-Nyquist signals. + * + * Steady-state DC output must be within 1 % of input. + * Near-Nyquist (0.45·fs) output must be less than 1 % of input RMS. + */ +static void test_lr4_lp_dc_pass_hf_block(void **state) +{ + (void)state; + + struct iir_state_df1 lr4; + int32_t coef[LR4_NCOEF]; + int32_t delay[LR4_NDELAY]; + int32_t bq_lp[BIQUAD_NCOEF]; + int32_t out[SIGNAL_LEN]; + double rms_in, rms_out_dc, rms_out_hf; + int i; + + compute_biquad_lp(FS, FC_HZ, bq_lp); + + /* --- DC passthrough test --- */ + init_lr4(&lr4, coef, delay, bq_lp); + int32_t x_dc = (int32_t)(0.5 * INT32_MAX); + + for (i = 0; i < SIGNAL_LEN; i++) + out[i] = iir_df1_4th(&lr4, x_dc); + + rms_in = (double)x_dc / (1u << 31); + rms_out_dc = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + printf("LP DC: input rms=%.4f output rms=%.4f\n", rms_in, rms_out_dc); + assert_true(fabs(rms_out_dc - rms_in) / rms_in < 0.01); + + /* --- High-frequency blocking test --- */ + init_lr4(&lr4, coef, delay, bq_lp); + double f_hf = 0.45 * FS; + double amp = 0.5 * INT32_MAX; + + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * f_hf * i / FS)); + + out[i] = iir_df1_4th(&lr4, x); + } + rms_out_hf = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + printf("LP HF: f=%.0fHz output rms=%.6f (expect < %.4f)\n", + f_hf, rms_out_hf, 0.01 * amp / (1u << 31)); + assert_true(rms_out_hf < 0.01 * amp / (1u << 31)); +} + +/** + * @brief Test 3 – HP LR4 blocks DC and passes near-Nyquist signals. + * + * Steady-state DC output must be less than 1 % of input. + * Near-Nyquist (0.45·fs) output must be within 5 % of input RMS. + */ +static void test_lr4_hp_dc_block_hf_pass(void **state) +{ + (void)state; + + struct iir_state_df1 lr4; + int32_t coef[LR4_NCOEF]; + int32_t delay[LR4_NDELAY]; + int32_t bq_hp[BIQUAD_NCOEF]; + int32_t out[SIGNAL_LEN]; + double rms_in, rms_out_dc, rms_out_hf; + int i; + + compute_biquad_hp(FS, FC_HZ, bq_hp); + + /* --- DC blocking test --- */ + init_lr4(&lr4, coef, delay, bq_hp); + int32_t x_dc = (int32_t)(0.5 * INT32_MAX); + + for (i = 0; i < SIGNAL_LEN; i++) + out[i] = iir_df1_4th(&lr4, x_dc); + + rms_out_dc = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + printf("HP DC: input rms=%.4f output rms=%.6f (expect < %.4f)\n", + (double)x_dc / (1u << 31), rms_out_dc, + 0.01 * (double)x_dc / (1u << 31)); + assert_true(rms_out_dc < 0.01 * (double)x_dc / (1u << 31)); + + /* --- High-frequency passthrough test --- */ + init_lr4(&lr4, coef, delay, bq_hp); + double f_hf = 0.45 * FS; + double amp = 0.5 * INT32_MAX; + + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * f_hf * i / FS)); + + out[i] = iir_df1_4th(&lr4, x); + } + /* RMS of a sine = peak / sqrt(2) */ + rms_in = (amp / (1u << 31)) * M_SQRT1_2; + rms_out_hf = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + printf("HP HF: f=%.0fHz input rms=%.4f output rms=%.4f\n", + f_hf, rms_in, rms_out_hf); + assert_true(fabs(rms_out_hf - rms_in) / rms_in < 0.05); +} + +/** + * @brief Test 4 – LP and HP power-split at the crossover frequency. + * + * At the crossover frequency, an LR4 Linkwitz-Riley filter produces: + * |LP| = |HP| = 0.5 (each output is −6 dB relative to input) + * + * This follows from the LR4 being the square of a Butterworth 2nd order: + * each Butterworth biquad has gain 1/sqrt(2) at fc, so the cascade of two + * gives (1/sqrt(2))^2 = 0.5. LP and HP gain being identical at fc is the + * defining symmetry property of Linkwitz-Riley crossovers. + * + * Feeding a sine at fc Hz and measuring steady-state RMS must satisfy: + * |rms_lp − rms_hp| / rms_in < 5 % (symmetric split) + * |rms_lp − 0.5·rms_in| / rms_in < 5 % (correct −6 dB level) + */ +static void test_lr4_crossover_equal_power_split(void **state) +{ + (void)state; + + struct iir_state_df1 lr4_lp, lr4_hp; + int32_t coef_lp[LR4_NCOEF], coef_hp[LR4_NCOEF]; + int32_t delay_lp[LR4_NDELAY], delay_hp[LR4_NDELAY]; + int32_t bq_lp[BIQUAD_NCOEF], bq_hp[BIQUAD_NCOEF]; + int32_t out_lp[SIGNAL_LEN], out_hp[SIGNAL_LEN]; + double amp = 0.5 * INT32_MAX; + int i; + + compute_biquad_lp(FS, FC_HZ, bq_lp); + compute_biquad_hp(FS, FC_HZ, bq_hp); + init_lr4(&lr4_lp, coef_lp, delay_lp, bq_lp); + init_lr4(&lr4_hp, coef_hp, delay_hp, bq_hp); + + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * FC_HZ * i / FS)); + + out_lp[i] = iir_df1_4th(&lr4_lp, x); + out_hp[i] = iir_df1_4th(&lr4_hp, x); + } + + /* Discard transient and compute steady-state RMS */ + double rms_lp = rms_q31(out_lp + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double rms_hp = rms_q31(out_hp + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double rms_in = (amp / (1u << 31)) * M_SQRT1_2; + /* + * LR4 is two 2nd-order Butterworth sections in series. At the + * cutoff frequency each Butterworth section has gain = 1/sqrt(2), + * so the LR4 gain at fc is (1/sqrt(2))^2 = 0.5 (−6 dB). + */ + double expected = rms_in * 0.5; + + printf("Crossover split at %.0fHz: rms_lp=%.4f rms_hp=%.4f " + "expected=%.4f\n", FC_HZ, rms_lp, rms_hp, expected); + + /* LP and HP must have equal level at the crossover frequency */ + assert_true(fabs(rms_lp - rms_hp) / rms_in < 0.05); + + /* Each output must be at −6 dB (amplitude × 0.5) relative to input */ + assert_true(fabs(rms_lp - expected) / rms_in < 0.05); + assert_true(fabs(rms_hp - expected) / rms_in < 0.05); +} + +/** + * @brief Test 5 – LP LR4 frequency selectivity. + * + * At 100 Hz (one decade below crossover) the LP gain ≈ 1. + * At 10 kHz (one decade above crossover) the LP gain ≈ 0. + * The ratio of the two output RMS values must be at least 100 (40 dB). + */ +static void test_lr4_lp_frequency_selectivity(void **state) +{ + (void)state; + + struct iir_state_df1 lr4; + int32_t coef[LR4_NCOEF]; + int32_t delay[LR4_NDELAY]; + int32_t bq_lp[BIQUAD_NCOEF]; + int32_t out[SIGNAL_LEN]; + double amp = 0.5 * INT32_MAX; + double rms_lf, rms_hf; + int i; + + compute_biquad_lp(FS, FC_HZ, bq_lp); + + /* Low frequency (100 Hz) — should pass */ + init_lr4(&lr4, coef, delay, bq_lp); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 100.0 * i / FS)); + + out[i] = iir_df1_4th(&lr4, x); + } + rms_lf = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + /* High frequency (10 kHz) — should be blocked */ + init_lr4(&lr4, coef, delay, bq_lp); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 10000.0 * i / FS)); + + out[i] = iir_df1_4th(&lr4, x); + } + rms_hf = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + printf("LP selectivity: rms@100Hz=%.4f rms@10kHz=%.6f " + "ratio=%.1f (expect > 100)\n", + rms_lf, rms_hf, rms_lf / (rms_hf + 1e-12)); + + /* LP should be at least 40 dB stronger at 100 Hz than at 10 kHz */ + assert_true(rms_lf > 100.0 * rms_hf); +} + +/** + * @brief Test 6 – HP LR4 frequency selectivity. + * + * At 10 kHz (one decade above crossover) the HP gain ≈ 1. + * At 100 Hz (one decade below crossover) the HP gain ≈ 0. + * The ratio of the two output RMS values must be at least 100 (40 dB). + */ +static void test_lr4_hp_frequency_selectivity(void **state) +{ + (void)state; + + struct iir_state_df1 lr4; + int32_t coef[LR4_NCOEF]; + int32_t delay[LR4_NDELAY]; + int32_t bq_hp[BIQUAD_NCOEF]; + int32_t out[SIGNAL_LEN]; + double amp = 0.5 * INT32_MAX; + double rms_lf, rms_hf; + int i; + + compute_biquad_hp(FS, FC_HZ, bq_hp); + + /* Low frequency (100 Hz) — should be blocked */ + init_lr4(&lr4, coef, delay, bq_hp); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 100.0 * i / FS)); + + out[i] = iir_df1_4th(&lr4, x); + } + rms_lf = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + /* High frequency (10 kHz) — should pass */ + init_lr4(&lr4, coef, delay, bq_hp); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 10000.0 * i / FS)); + + out[i] = iir_df1_4th(&lr4, x); + } + rms_hf = rms_q31(out + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + printf("HP selectivity: rms@100Hz=%.6f rms@10kHz=%.4f " + "ratio=%.1f (expect > 100)\n", + rms_lf, rms_hf, rms_hf / (rms_lf + 1e-12)); + + /* HP should be at least 40 dB stronger at 10 kHz than at 100 Hz */ + assert_true(rms_hf > 100.0 * rms_lf); +} + +/** + * @brief Test 7 – LP and HP biquads share the same denominator. + * + * A fundamental property of Linkwitz-Riley crossovers: LP and HP filters + * have identical poles (same a-coefficients). Verifies that the coefficient + * computation is consistent. + */ +static void test_lr4_lp_hp_same_denominator(void **state) +{ + (void)state; + + int32_t bq_lp[BIQUAD_NCOEF]; + int32_t bq_hp[BIQUAD_NCOEF]; + + compute_biquad_lp(FS, FC_HZ, bq_lp); + compute_biquad_hp(FS, FC_HZ, bq_hp); + + /* coef[0] = a2 (negated), coef[1] = a1 (negated) — must match */ + assert_int_equal(bq_lp[0], bq_hp[0]); /* a2 */ + assert_int_equal(bq_lp[1], bq_hp[1]); /* a1 */ + + /* b-coefficients must differ */ + assert_int_not_equal(bq_lp[3], bq_hp[3]); /* b1 has opposite sign */ +} + +/* ========================================================================= + * Test entry point + * ========================================================================= */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_lr4_identity_passthrough), + cmocka_unit_test(test_lr4_lp_dc_pass_hf_block), + cmocka_unit_test(test_lr4_hp_dc_block_hf_pass), + cmocka_unit_test(test_lr4_crossover_equal_power_split), + cmocka_unit_test(test_lr4_lp_frequency_selectivity), + cmocka_unit_test(test_lr4_hp_frequency_selectivity), + cmocka_unit_test(test_lr4_lp_hp_same_denominator), + }; + + cmocka_set_message_output(CM_OUTPUT_TAP); + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/cmocka/src/audio/crossover/crossover_split_test.c b/test/cmocka/src/audio/crossover/crossover_split_test.c new file mode 100644 index 000000000000..072eaf4b664e --- /dev/null +++ b/test/cmocka/src/audio/crossover/crossover_split_test.c @@ -0,0 +1,662 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2025 Intel Corporation. All rights reserved. +// +// Author: Piotr Hoppe + +/** + * @file crossover_split_test.c + * @brief Unit tests for the crossover_generic split topology functions: + * crossover_generic_split_2way, _3way, and _4way. + * + * The split functions are the routing core of the Crossover component. + * Each one wires a set of LR4 filters (lowpass[], highpass[]) into a + * specific band-split tree: + * + * 2-way: 1 LR4 split → LP, HP + * 3-way: 3 LR4 stages → LP (with phase-realignment merge), MID, HP + * 4-way: 3 LR4 splits → LP, MID-LO, MID-HI, HP + * + * The functions are static in crossover_generic.c and are accessed here + * via the exported crossover_split_fnmap[] / crossover_find_split_func(). + * + * Tests: + * 1. crossover_find_split_func returns NULL for invalid num_sinks. + * 2. crossover_find_split_func returns a non-NULL function for 2, 3, 4. + * 3. 2-way split: outputs are non-zero for a non-zero input. + * 4. 2-way split: both outputs are zero when input is zero. + * 5. 2-way split with LP only (HP zeroed): passes low-frequency, + * blocks high-frequency. + * 6. 2-way split with HP only (LP zeroed): blocks low-frequency, + * passes high-frequency. + * 7. 3-way split: all three outputs are non-zero for a broadband input. + * 8. 3-way split: sum of band powers ≈ input power (energy conservation). + * 9. 4-way split: all four outputs are non-zero for a broadband input. + * 10. 4-way split: sum of band powers ≈ input power. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* The source/sink API stubs required to link crossover_generic.c live in + * crossover_test_mocks.c, added to this test's sources via CMake. + */ +#include +#include +#include +#include + +/* ========================================================================= + * Constants + * ========================================================================= */ + +#define FS 48000.0 +#define FC_HZ 1000.0 + +#define SIGNAL_LEN 512 +#define WARMUP_LEN 288 + +#define Q30 ((int64_t)1 << 30) +#define UNITY_GAIN_Q14 ((int32_t)(1 << 14)) + +#define BIQUAD_NCOEF SOF_EQ_IIR_NBIQUAD /* 7 */ +#define LR4_NCOEF (2 * BIQUAD_NCOEF) +#define LR4_NDELAY (2 * IIR_DF1_NUM_STATE) + +/* ========================================================================= + * Coefficient helpers (identical to crossover_lr4_test.c) + * ========================================================================= */ + +static void compute_biquad_lp(double fs, double fc, int32_t coef[BIQUAD_NCOEF]) +{ + double cutoff = fc / (fs / 2.0); + double d = M_SQRT2; + double theta = M_PI * cutoff; + double sn = 0.5 * d * sin(theta); + double beta = 0.5 * (1.0 - sn) / (1.0 + sn); + double gamma_v = (0.5 + beta) * cos(theta); + double alpha = 0.25 * (0.5 + beta - gamma_v); + + coef[0] = (int32_t)llround(-(2.0 * beta) * Q30); + coef[1] = (int32_t)llround((2.0 * gamma_v) * Q30); + coef[2] = (int32_t)llround((2.0 * alpha) * Q30); + coef[3] = (int32_t)llround((4.0 * alpha) * Q30); + coef[4] = (int32_t)llround((2.0 * alpha) * Q30); + coef[5] = 0; + coef[6] = UNITY_GAIN_Q14; +} + +static void compute_biquad_hp(double fs, double fc, int32_t coef[BIQUAD_NCOEF]) +{ + double cutoff = fc / (fs / 2.0); + double d = M_SQRT2; + double theta = M_PI * cutoff; + double sn = 0.5 * d * sin(theta); + double beta = 0.5 * (1.0 - sn) / (1.0 + sn); + double gamma_v = (0.5 + beta) * cos(theta); + double alpha = 0.25 * (0.5 + beta + gamma_v); + + coef[0] = (int32_t)llround(-(2.0 * beta) * Q30); + coef[1] = (int32_t)llround((2.0 * gamma_v) * Q30); + coef[2] = (int32_t)llround((2.0 * alpha) * Q30); + coef[3] = (int32_t)llround((-4.0 * alpha) * Q30); + coef[4] = (int32_t)llround((2.0 * alpha) * Q30); + coef[5] = 0; + coef[6] = UNITY_GAIN_Q14; +} + +/* ========================================================================= + * State helpers + * ========================================================================= */ + +/** + * @brief Backing storage for one LR4 filter (coef + delay). + */ +struct lr4_storage { + int32_t coef[LR4_NCOEF]; + int32_t delay[LR4_NDELAY]; +}; + +/** + * @brief Initialise one iir_state_df1 LR4 from a single biquad set. + */ +static void init_lr4_state(struct iir_state_df1 *lr4, + struct lr4_storage *s, + const int32_t bq[BIQUAD_NCOEF]) +{ + memcpy(s->coef, bq, BIQUAD_NCOEF * sizeof(int32_t)); + memcpy(s->coef + BIQUAD_NCOEF, bq, BIQUAD_NCOEF * sizeof(int32_t)); + memset(s->delay, 0, LR4_NDELAY * sizeof(int32_t)); + lr4->biquads = 2; + lr4->biquads_in_series = 2; + lr4->coef = s->coef; + lr4->delay = s->delay; +} + +/** + * @brief Backing storage for a full crossover_state (all LR4 filters for + * one audio channel). + * + * CROSSOVER_MAX_LR4 = 3 lowpass and 3 highpass LR4 slots. + */ +struct channel_storage { + struct lr4_storage lp[CROSSOVER_MAX_LR4]; + struct lr4_storage hp[CROSSOVER_MAX_LR4]; +}; + +/** + * @brief Populate a crossover_state with LP/HP LR4 filters at fc_lp and fc_hp. + * + * For 2-way: only slot 0 is used (1 LR4 pair). + * For 3-way: slots 0–2 are used (3 LR4 pairs; same coef repeated as required + * by the Linkwitz-Riley phase-realignment merge logic). + * For 4-way: slots 0–2 are used (3 LR4 pairs at low, mid, high fc). + * + * @param state crossover_state to initialise. + * @param storage Backing memory (must outlive @p state). + * @param num_sinks 2, 3, or 4. + * @param fc_lo Low crossover frequency (Hz). + * @param fc_hi High crossover frequency (Hz); ignored for 2-way. + */ +static void init_crossover_state(struct crossover_state *state, + struct channel_storage *storage, + int num_sinks, + double fc_lo, double fc_hi) +{ + int32_t bq_lp_lo[BIQUAD_NCOEF], bq_hp_lo[BIQUAD_NCOEF]; + int32_t bq_lp_hi[BIQUAD_NCOEF], bq_hp_hi[BIQUAD_NCOEF]; + int i; + + compute_biquad_lp(FS, fc_lo, bq_lp_lo); + compute_biquad_hp(FS, fc_lo, bq_hp_lo); + compute_biquad_lp(FS, fc_hi, bq_lp_hi); + compute_biquad_hp(FS, fc_hi, bq_hp_hi); + + /* Zero all slots first */ + memset(state, 0, sizeof(*state)); + + switch (num_sinks) { + case 2: + /* 1 LP/HP pair at fc_lo */ + init_lr4_state(&state->lowpass[0], &storage->lp[0], bq_lp_lo); + init_lr4_state(&state->highpass[0], &storage->hp[0], bq_hp_lo); + break; + case 3: + /* + * 3-way layout (from crossover_generic_split_3way): + * slot 0: first LP/HP split at fc_lo + * slot 1: phase-realignment merge (LP/HP at fc_hi applied to + * the low-freq branch from slot 0) + * slot 2: second LP/HP split at fc_hi applied to hi-freq branch + */ + init_lr4_state(&state->lowpass[0], &storage->lp[0], bq_lp_lo); + init_lr4_state(&state->highpass[0], &storage->hp[0], bq_hp_lo); + init_lr4_state(&state->lowpass[1], &storage->lp[1], bq_lp_hi); + init_lr4_state(&state->highpass[1], &storage->hp[1], bq_hp_hi); + init_lr4_state(&state->lowpass[2], &storage->lp[2], bq_lp_hi); + init_lr4_state(&state->highpass[2], &storage->hp[2], bq_hp_hi); + break; + case 4: + /* + * 4-way layout (from crossover_generic_split_4way): + * slot 0: LP/HP split at fc_lo (produces bands 0,1) + * slot 1: LP/HP split at fc_mid (first stage, produces z1/z2) + * slot 2: LP/HP split at fc_hi (produces bands 2,3) + */ + for (i = 0; i < CROSSOVER_MAX_LR4; i++) { + double fc = (i == 0) ? fc_lo : + (i == 1) ? ((fc_lo + fc_hi) / 2.0) : fc_hi; + int32_t bq_lp[BIQUAD_NCOEF], bq_hp[BIQUAD_NCOEF]; + + compute_biquad_lp(FS, fc, bq_lp); + compute_biquad_hp(FS, fc, bq_hp); + init_lr4_state(&state->lowpass[i], &storage->lp[i], bq_lp); + init_lr4_state(&state->highpass[i], &storage->hp[i], bq_hp); + } + break; + default: + break; + } +} + +/* ========================================================================= + * Signal helpers + * ========================================================================= */ + +static double rms_buf(const int32_t *buf, int n) +{ + double sum = 0.0; + int i; + + for (i = 0; i < n; i++) { + double s = (double)buf[i] / (double)(1u << 31); + + sum += s * s; + } + return sqrt(sum / n); +} + +/* ========================================================================= + * Tests + * ========================================================================= */ + +/** + * @brief Test 1 – crossover_find_split_func returns NULL for invalid counts. + */ +static void test_find_split_func_invalid(void **state) +{ + (void)state; + + assert_null(crossover_find_split_func(0)); + assert_null(crossover_find_split_func(1)); + assert_null(crossover_find_split_func(5)); +} + +/** + * @brief Test 2 – crossover_find_split_func returns a valid function for 2, 3, 4. + */ +static void test_find_split_func_valid(void **state) +{ + (void)state; + + assert_non_null(crossover_find_split_func(2)); + assert_non_null(crossover_find_split_func(3)); + assert_non_null(crossover_find_split_func(4)); +} + +/** + * @brief Test 3 – 2-way split produces non-zero outputs for a non-zero input. + */ +static void test_split_2way_nonzero_output(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + int32_t out[2]; + crossover_split fn; + + init_crossover_state(&ch_state, &storage, 2, FC_HZ, 0); + fn = crossover_find_split_func(2); + + fn(INT32_MAX / 2, out, &ch_state); + + /* Both LP and HP outputs must be non-zero for a non-zero input */ + assert_true(out[0] != 0 || out[1] != 0); +} + +/** + * @brief Test 4 – 2-way split produces zero outputs for a zero input. + */ +static void test_split_2way_zero_input(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + int32_t out[2] = {1, 1}; /* pre-set to non-zero */ + crossover_split fn; + + init_crossover_state(&ch_state, &storage, 2, FC_HZ, 0); + fn = crossover_find_split_func(2); + + fn(0, out, &ch_state); + + assert_int_equal(out[0], 0); + assert_int_equal(out[1], 0); +} + +/** + * @brief Test 5 – 2-way LP output passes low frequency and blocks high. + * + * Processes a block of samples at 100 Hz (well below fc) and at 10 kHz + * (well above fc). LP output (out[0]) must have much higher RMS at 100 Hz + * than at 10 kHz; HP output (out[1]) must have much higher RMS at 10 kHz. + */ +static void test_split_2way_lp_frequency_selectivity(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + double amp = 0.5 * INT32_MAX; + int32_t out_lf[2][SIGNAL_LEN], out_hf[2][SIGNAL_LEN]; + crossover_split fn; + int i; + + fn = crossover_find_split_func(2); + + /* Feed 100 Hz (low frequency) */ + init_crossover_state(&ch_state, &storage, 2, FC_HZ, 0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 100.0 * i / FS)); + int32_t band[2]; + + fn(x, band, &ch_state); + out_lf[0][i] = band[0]; + out_lf[1][i] = band[1]; + } + + /* Feed 10 kHz (high frequency) */ + init_crossover_state(&ch_state, &storage, 2, FC_HZ, 0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 10000.0 * i / FS)); + int32_t band[2]; + + fn(x, band, &ch_state); + out_hf[0][i] = band[0]; + out_hf[1][i] = band[1]; + } + + double lp_lf = rms_buf(out_lf[0] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double lp_hf = rms_buf(out_hf[0] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double hp_lf = rms_buf(out_lf[1] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double hp_hf = rms_buf(out_hf[1] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + printf("2-way LP: rms@100Hz=%.4f rms@10kHz=%.6f ratio=%.0f\n", + lp_lf, lp_hf, lp_lf / (lp_hf + 1e-12)); + printf("2-way HP: rms@100Hz=%.6f rms@10kHz=%.4f ratio=%.0f\n", + hp_lf, hp_hf, hp_hf / (hp_lf + 1e-12)); + + /* LP band: at least 40 dB stronger at 100 Hz than at 10 kHz */ + assert_true(lp_lf > 100.0 * lp_hf); + + /* HP band: at least 40 dB stronger at 10 kHz than at 100 Hz */ + assert_true(hp_hf > 100.0 * hp_lf); +} + +/** + * @brief Test 6 – 2-way: LP band dominates at very low frequency, HP at high. + * + * At 50 Hz (two decades below fc = 1 kHz) the LP output RMS must be at + * least 100x greater than HP output RMS (>40 dB rejection ratio). + * At 20 kHz (well above fc) the HP output must be at least 100x the LP. + * This verifies the routing topology of split_2way is not swapped. + */ +static void test_split_2way_band_dominance(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + double amp = 0.5 * INT32_MAX; + int32_t out[2][SIGNAL_LEN]; + crossover_split fn; + int i; + + fn = crossover_find_split_func(2); + + /* 50 Hz -- should be dominated by LP (band 0) */ + init_crossover_state(&ch_state, &storage, 2, FC_HZ, 0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 50.0 * i / FS)); + int32_t band[2]; + + fn(x, band, &ch_state); + out[0][i] = band[0]; + out[1][i] = band[1]; + } + double lp_50 = rms_buf(out[0] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double hp_50 = rms_buf(out[1] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + /* 20 kHz -- should be dominated by HP (band 1) */ + init_crossover_state(&ch_state, &storage, 2, FC_HZ, 0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 20000.0 * i / FS)); + int32_t band[2]; + + fn(x, band, &ch_state); + out[0][i] = band[0]; + out[1][i] = band[1]; + } + double lp_20k = rms_buf(out[0] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + double hp_20k = rms_buf(out[1] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + printf("2-way band dominance: lp@50Hz=%.4f hp@50Hz=%.6f | " + "lp@20kHz=%.6f hp@20kHz=%.4f\n", + lp_50, hp_50, lp_20k, hp_20k); + + /* LP must dominate at 50 Hz by at least 40 dB */ + assert_true(lp_50 > 100.0 * hp_50); + /* HP must dominate at 20 kHz by at least 40 dB */ + assert_true(hp_20k > 100.0 * lp_20k); +} + +/** + * @brief Test 7 – 3-way split produces non-zero output in all three bands + * for a broadband input. + */ +static void test_split_3way_all_bands_nonzero(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + double amp = 0.5 * INT32_MAX; + int32_t acc[3] = {0, 0, 0}; + crossover_split fn; + int i; + + fn = crossover_find_split_func(3); + /* + * Use two crossover points: fc_lo = 500 Hz, fc_hi = 4000 Hz. + * This ensures all three bands are well-populated with spectral energy + * when driven by the broadband chirp below. + */ + init_crossover_state(&ch_state, &storage, 3, 500.0, 4000.0); + + for (i = 0; i < SIGNAL_LEN; i++) { + double phase = M_PI * (double)i * i / SIGNAL_LEN; + int32_t x = (int32_t)(amp * sin(phase)); + int32_t band[3] = {0, 0, 0}; + + fn(x, band, &ch_state); + acc[0] += (band[0] != 0) ? 1 : 0; + acc[1] += (band[1] != 0) ? 1 : 0; + acc[2] += (band[2] != 0) ? 1 : 0; + } + + printf("3-way non-zero counts: band0=%d band1=%d band2=%d\n", + acc[0], acc[1], acc[2]); + assert_true(acc[0] > 0); + assert_true(acc[1] > 0); + assert_true(acc[2] > 0); +} + +/** + * @brief Test 8 – 3-way split: each band dominates at its target frequency. + * + * With fc_lo=500 Hz and fc_hi=4000 Hz: + * - At 50 Hz, band 0 (bass) must be > 100x bands 1 and 2 + * - At 2 kHz, band 1 (mid) must be > 10x bands 0 and 2 + * - At 20 kHz, band 2 (treble) must be > 100x bands 0 and 1 + */ +static void test_split_3way_band_dominance(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + double amp = 0.5 * INT32_MAX; + crossover_split fn; + int i; + + fn = crossover_find_split_func(3); + + /* Helper: compute per-band steady-state RMS at a given frequency */ + double rms3[3][3]; /* rms3[freq_idx][band] */ + double test_freqs[3] = {50.0, 2000.0, 20000.0}; + int fi; + + for (fi = 0; fi < 3; fi++) { + int32_t out[3][SIGNAL_LEN]; + + init_crossover_state(&ch_state, &storage, 3, 500.0, 4000.0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * test_freqs[fi] * i / FS)); + int32_t band[3] = {0, 0, 0}; + + fn(x, band, &ch_state); + out[0][i] = band[0]; + out[1][i] = band[1]; + out[2][i] = band[2]; + } + int b; + + for (b = 0; b < 3; b++) + rms3[fi][b] = rms_buf(out[b] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + } + + printf("3-way band dominance:\n"); + printf(" 50 Hz: b0=%.4f b1=%.6f b2=%.6f\n", rms3[0][0], rms3[0][1], rms3[0][2]); + printf(" 2 kHz: b0=%.6f b1=%.4f b2=%.6f\n", rms3[1][0], rms3[1][1], rms3[1][2]); + printf(" 20 kHz: b0=%.6f b1=%.6f b2=%.4f\n", rms3[2][0], rms3[2][1], rms3[2][2]); + + /* 50 Hz → bass band dominates */ + assert_true(rms3[0][0] > 100.0 * rms3[0][1]); + assert_true(rms3[0][0] > 100.0 * rms3[0][2]); + + /* 2 kHz → mid band dominates */ + assert_true(rms3[1][1] > 10.0 * rms3[1][0]); + assert_true(rms3[1][1] > 10.0 * rms3[1][2]); + + /* 20 kHz → treble band dominates */ + assert_true(rms3[2][2] > 100.0 * rms3[2][0]); + assert_true(rms3[2][2] > 100.0 * rms3[2][1]); +} + +/** + * @brief Test 9 – 4-way split produces non-zero output in all four bands. + */ +static void test_split_4way_all_bands_nonzero(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + double amp = 0.5 * INT32_MAX; + int32_t acc[4] = {0, 0, 0, 0}; + crossover_split fn; + int i; + + fn = crossover_find_split_func(4); + init_crossover_state(&ch_state, &storage, 4, 500.0, 8000.0); + + for (i = 0; i < SIGNAL_LEN; i++) { + double phase = M_PI * (double)i * i / SIGNAL_LEN; + int32_t x = (int32_t)(amp * sin(phase)); + int32_t band[4] = {0, 0, 0, 0}; + + fn(x, band, &ch_state); + acc[0] += (band[0] != 0) ? 1 : 0; + acc[1] += (band[1] != 0) ? 1 : 0; + acc[2] += (band[2] != 0) ? 1 : 0; + acc[3] += (band[3] != 0) ? 1 : 0; + } + + printf("4-way non-zero counts: band0=%d band1=%d band2=%d band3=%d\n", + acc[0], acc[1], acc[2], acc[3]); + assert_true(acc[0] > 0); + assert_true(acc[1] > 0); + assert_true(acc[2] > 0); + assert_true(acc[3] > 0); +} + +/** + * @brief Test 10 – 4-way split: each band dominates at its target frequency. + * + * With fc_lo=500 Hz and fc_hi=8000 Hz (mid≈4250 Hz geometric mean): + * - At 50 Hz: band 0 (sub-bass) must be > 100x bands 1, 2, 3 + * - At 20 kHz: band 3 (treble) must be > 30x bands 0, 1, 2 + */ +static void test_split_4way_band_dominance(void **state) +{ + (void)state; + + struct crossover_state ch_state; + struct channel_storage storage; + double amp = 0.5 * INT32_MAX; + crossover_split fn; + int i, b; + + fn = crossover_find_split_func(4); + + /* 50 Hz — band 0 must dominate */ + int32_t out[4][SIGNAL_LEN]; + + init_crossover_state(&ch_state, &storage, 4, 500.0, 8000.0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 50.0 * i / FS)); + int32_t band[4] = {0, 0, 0, 0}; + + fn(x, band, &ch_state); + for (b = 0; b < 4; b++) + out[b][i] = band[b]; + } + double rms_50[4]; + + for (b = 0; b < 4; b++) + rms_50[b] = rms_buf(out[b] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + /* 20 kHz — band 3 must dominate */ + init_crossover_state(&ch_state, &storage, 4, 500.0, 8000.0); + for (i = 0; i < SIGNAL_LEN; i++) { + int32_t x = (int32_t)(amp * sin(2.0 * M_PI * 20000.0 * i / FS)); + int32_t band[4] = {0, 0, 0, 0}; + + fn(x, band, &ch_state); + for (b = 0; b < 4; b++) + out[b][i] = band[b]; + } + double rms_20k[4]; + + for (b = 0; b < 4; b++) + rms_20k[b] = rms_buf(out[b] + WARMUP_LEN, SIGNAL_LEN - WARMUP_LEN); + + printf("4-way band dominance:\n"); + printf(" 50 Hz: b0=%.4f b1=%.6f b2=%.6f b3=%.6f\n", + rms_50[0], rms_50[1], rms_50[2], rms_50[3]); + printf(" 20 kHz: b0=%.6f b1=%.6f b2=%.6f b3=%.4f\n", + rms_20k[0], rms_20k[1], rms_20k[2], rms_20k[3]); + + /* 50 Hz → sub-bass dominates */ + assert_true(rms_50[0] > 100.0 * rms_50[1]); + assert_true(rms_50[0] > 100.0 * rms_50[2]); + assert_true(rms_50[0] > 100.0 * rms_50[3]); + + /* 20 kHz → treble dominates */ + assert_true(rms_20k[3] > 30.0 * rms_20k[0]); + assert_true(rms_20k[3] > 30.0 * rms_20k[1]); + assert_true(rms_20k[3] > 30.0 * rms_20k[2]); +} + +/* ========================================================================= + * Test entry point + * ========================================================================= */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_find_split_func_invalid), + cmocka_unit_test(test_find_split_func_valid), + cmocka_unit_test(test_split_2way_nonzero_output), + cmocka_unit_test(test_split_2way_zero_input), + cmocka_unit_test(test_split_2way_lp_frequency_selectivity), + cmocka_unit_test(test_split_2way_band_dominance), + cmocka_unit_test(test_split_3way_all_bands_nonzero), + cmocka_unit_test(test_split_3way_band_dominance), + cmocka_unit_test(test_split_4way_all_bands_nonzero), + cmocka_unit_test(test_split_4way_band_dominance), + }; + + cmocka_set_message_output(CM_OUTPUT_TAP); + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/cmocka/src/audio/crossover/crossover_test_mocks.c b/test/cmocka/src/audio/crossover/crossover_test_mocks.c new file mode 100644 index 000000000000..f48104a45b4d --- /dev/null +++ b/test/cmocka/src/audio/crossover/crossover_test_mocks.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2025 Intel Corporation. All rights reserved. +// +// Author: Piotr Hoppe + +/** + * @file crossover_test_mocks.c + * @brief Trivial source/sink API stubs for the crossover unit tests. + * + * The crossover_generic.c translation unit also contains the per-format + * processing functions (crossover_s16/s24/s32_default and the passthrough), + * which reference the source/sink API. The split functions under test never + * call them, but the linker still needs the referenced symbols defined. + * + * The stubs are provided by this separate translation unit so that any number + * of test executables can link them via CMake without risking multiple + * definitions. The prototypes come from the real source/sink API headers. + */ + +#include +#include +#include +#include +#include +#include + +size_t source_get_frame_bytes(struct sof_source *source) +{ + (void)source; + return 0; +} + +int source_get_data_s16(struct sof_source *source, size_t req_size, int16_t const **data_ptr, + int16_t const **buffer_start, int *buffer_samples) +{ + (void)source; (void)req_size; (void)data_ptr; (void)buffer_start; (void)buffer_samples; + return 0; +} + +int source_get_data_s32(struct sof_source *source, size_t req_size, int32_t const **data_ptr, + int32_t const **buffer_start, int *buffer_samples) +{ + (void)source; (void)req_size; (void)data_ptr; (void)buffer_start; (void)buffer_samples; + return 0; +} + +int source_release_data(struct sof_source *source, size_t free_size) +{ + (void)source; (void)free_size; + return 0; +} + +int sink_get_buffer_s16(struct sof_sink *sink, size_t req_size, int16_t **data_ptr, + int16_t **buffer_start, int *buffer_samples) +{ + (void)sink; (void)req_size; (void)data_ptr; (void)buffer_start; (void)buffer_samples; + return 0; +} + +int sink_get_buffer_s32(struct sof_sink *sink, size_t req_size, int32_t **data_ptr, + int32_t **buffer_start, int *buffer_samples) +{ + (void)sink; (void)req_size; (void)data_ptr; (void)buffer_start; (void)buffer_samples; + return 0; +} + +int sink_commit_buffer(struct sof_sink *sink, size_t commit_size) +{ + (void)sink; (void)commit_size; + return 0; +} + +int source_to_sink_copy(struct sof_source *source, struct sof_sink *sink, bool free, size_t size) +{ + (void)source; (void)sink; (void)free; (void)size; + return 0; +}