diff --git a/.github/workflows/zephyr-unit-tests.yml b/.github/workflows/zephyr-unit-tests.yml index 9a5707c91867..65d77e80dfe8 100644 --- a/.github/workflows/zephyr-unit-tests.yml +++ b/.github/workflows/zephyr-unit-tests.yml @@ -52,8 +52,12 @@ jobs: run: | cd workspace export ZEPHYR_TOOLCHAIN_VARIANT=host/llvm - west twister --testsuite-root sof/test/ztest/unit/ --platform native_sim --verbose \ - --inline-logs + # DEBUGINFOD_URLS must stay empty: otherwise llvm-symbolizer queries + # debuginfod while LSan prints a leak report, which takes longer than + # twister's post-verdict grace period. Twister then kills the binary, + # ignores its exit code and reports the leaking suite as passed. + DEBUGINFOD_URLS= west twister --testsuite-root sof/test/ztest/unit/ \ + --platform native_sim --enable-asan --enable-lsan --verbose --inline-logs # This part is commented out because it is not needed at the moment. # - name: Install Zephyr SDK # run: | diff --git a/test/ztest/unit/common/mod_alloc.c b/test/ztest/unit/common/mod_alloc.c new file mode 100644 index 000000000000..db58dddb939d --- /dev/null +++ b/test/ztest/unit/common/mod_alloc.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// These contents may have been developed with support from one or more +// Intel-operated generative artificial intelligence solutions. +// +// Module resource allocator for ztest unit suites, see mod_alloc.h. + +#include + +#include +#include + +#include +#include +#include +#include + +#include "mod_alloc.h" + +/* + * The definitions below are named after the mod_alloc() API, not after its + * z_impl_*() back ends. generic.h only maps the two onto each other without + * CONFIG_USERSPACE; with userspace the API becomes a set of __syscall + * declarations and these definitions would no longer replace it. + */ +BUILD_ASSERT(!IS_ENABLED(CONFIG_USERSPACE), + "the ztest module allocator does not implement the mod_alloc() syscalls"); + +/** @brief Book-keeping entry for one live module allocation. */ +struct mod_alloc_test_block { + const void *ptr; + size_t size; +}; + +static struct mod_alloc_test_block mod_alloc_blocks[MOD_ALLOC_TEST_MAX_ALLOCATIONS]; +static size_t mod_alloc_count; +static size_t mod_alloc_bytes; + +/** + * @brief Record an allocation. + * @param ptr Allocated pointer. + * @param size Requested size in bytes. + * @return true when the allocation was recorded, false when the table is full. + */ +static bool mod_alloc_block_add(const void *ptr, size_t size) +{ + if (mod_alloc_count == ARRAY_SIZE(mod_alloc_blocks)) { + return false; + } + + mod_alloc_blocks[mod_alloc_count].ptr = ptr; + mod_alloc_blocks[mod_alloc_count].size = size; + mod_alloc_count++; + mod_alloc_bytes += size; + + return true; +} + +/** + * @brief Drop the record of an allocation. + * @param ptr Pointer to look up. + * @return true when @p ptr was a live allocation, false otherwise. + */ +static bool mod_alloc_block_del(const void *ptr) +{ + size_t i; + + for (i = 0; i < mod_alloc_count; i++) { + if (mod_alloc_blocks[i].ptr != ptr) { + continue; + } + + mod_alloc_bytes -= mod_alloc_blocks[i].size; + mod_alloc_count--; + mod_alloc_blocks[i] = mod_alloc_blocks[mod_alloc_count]; + + return true; + } + + return false; +} + +/** + * @brief Allocate and record memory, the back end of the mod_alloc() API. + * @param flags SOF_MEM_FLAG_* allocation flags. + * @param size Size in bytes, zero is rejected like in the production allocator. + * @param alignment Alignment in bytes, 0 for the default. + * @return Pointer to the allocated memory or NULL on failure. + */ +static void *mod_alloc_test_alloc(uint32_t flags, size_t size, size_t alignment) +{ + void *ptr; + + if (!size) { + return NULL; + } + + ptr = sof_ctx_alloc(NULL, flags, size, alignment); + if (!ptr) { + return NULL; + } + + if (!mod_alloc_block_add(ptr, size)) { + sof_ctx_free(NULL, ptr); + return NULL; + } + + return ptr; +} + +/** + * @brief Allocate an aligned buffer for a module. + * + * Uses the same flags as z_impl_mod_balloc_align(). + * + * @param mod Module the memory is allocated for, only used for book-keeping. + * @param size Size in bytes. + * @param alignment Alignment in bytes, 0 for the default. + * @return Pointer to the allocated memory or NULL on failure. + */ +void *mod_balloc_align(struct processing_module *mod, size_t size, size_t alignment) +{ + (void)mod; + + return mod_alloc_test_alloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_LARGE_BUFFER, size, alignment); +} + +/** + * @brief Allocate memory for a module with explicit allocator flags. + * @param mod Module the memory is allocated for, only used for book-keeping. + * @param flags SOF_MEM_FLAG_* allocation flags. + * @param size Size in bytes. + * @param alignment Alignment in bytes, 0 for the default. + * @return Pointer to the allocated memory or NULL on failure. + */ +void *mod_alloc_ext(struct processing_module *mod, uint32_t flags, size_t size, size_t alignment) +{ + (void)mod; + + return mod_alloc_test_alloc(flags, size, alignment); +} + +/** + * @brief Free memory allocated with mod_alloc_ext() or mod_balloc_align(). + * @param mod Module the memory was allocated for, only used for book-keeping. + * @param ptr Pointer to free, NULL is ignored. + * @return 0 on success, -EINVAL if @p ptr is not a live module allocation. + */ +int mod_free(struct processing_module *mod, const void *ptr) +{ + (void)mod; + + if (!ptr) { + return 0; + } + + if (!mod_alloc_block_del(ptr)) { + return -EINVAL; + } + + sof_ctx_free(NULL, (void *)ptr); + + return 0; +} + +/** + * @brief Free every allocation still held by a module. + * @param mod Module to release, this allocator does not track ownership so all + * live allocations are released. + */ +void mod_free_all(struct processing_module *mod) +{ + (void)mod; + + mod_alloc_test_free_all(); +} + +size_t mod_alloc_test_allocations(void) +{ + return mod_alloc_count; +} + +size_t mod_alloc_test_bytes(void) +{ + return mod_alloc_bytes; +} + +void mod_alloc_test_free_all(void) +{ + while (mod_alloc_count) { + mod_alloc_count--; + sof_ctx_free(NULL, (void *)mod_alloc_blocks[mod_alloc_count].ptr); + } + + mod_alloc_bytes = 0; +} + +void mod_alloc_test_after(void *fixture) +{ + size_t leaked = mod_alloc_count; + size_t leaked_bytes = mod_alloc_bytes; + + (void)fixture; + + /* Release the leak so that the next test case starts from a clean heap */ + mod_alloc_test_free_all(); + + zassert_equal(leaked, 0, "Test case leaked %zu module allocation(s), %zu bytes", leaked, + leaked_bytes); +} diff --git a/test/ztest/unit/common/mod_alloc.h b/test/ztest/unit/common/mod_alloc.h new file mode 100644 index 000000000000..bcdec783e4ec --- /dev/null +++ b/test/ztest/unit/common/mod_alloc.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright(c) 2026 Intel Corporation. + * + * These contents may have been developed with support from one or more + * Intel-operated generative artificial intelligence solutions. + */ + +#ifndef __TEST_ZTEST_UNIT_COMMON_MOD_ALLOC_H__ +#define __TEST_ZTEST_UNIT_COMMON_MOD_ALLOC_H__ + +#include + +/** + * @file + * + * Module resource allocator for ztest unit suites that exercise code calling + * the mod_alloc()/mod_free() API but do not link the production allocator in + * src/audio/module_adapter/module/generic.c (which pulls in the whole module + * adapter, IPC and objpool infrastructure). + * + * The implementation in mod_alloc.c forwards every allocation to the SOF heap + * through sof_ctx_alloc()/sof_ctx_free(), so a suite exercises whichever SOF + * allocator it links, for example the real zephyr/lib/alloc.c in either its + * Zephyr sys_heap or its native_sim host heap + * (CONFIG_SOF_NATIVE_SIM_HOST_HEAP) flavour. + * + * Like the production allocator it tracks the live allocations, so + * mod_free_all() works and a suite can detect leaked, double-freed or foreign + * pointers on its own, without relying on ASan/LSan and the host heap. The + * tracking is not thread safe; unit suites are single threaded. + */ + +/** @brief Number of live module allocations a suite may have at any time. */ +#ifndef MOD_ALLOC_TEST_MAX_ALLOCATIONS +#define MOD_ALLOC_TEST_MAX_ALLOCATIONS 128 +#endif + +/** + * @brief Return the number of module allocations that are currently live. + * @return Number of pointers handed out and not yet freed. + */ +size_t mod_alloc_test_allocations(void); + +/** + * @brief Return the number of bytes currently allocated by modules. + * @return Sum of the requested sizes of all live allocations. + */ +size_t mod_alloc_test_bytes(void); + +/** + * @brief Free every live module allocation, whichever module owns it. + * + * Used by mod_alloc_test_after() to keep test cases independent and available + * to suites that install their own ztest teardown. + */ +void mod_alloc_test_free_all(void); + +/** + * @brief ztest after-hook asserting that a test case leaked no module memory. + * + * Matches the ztest after_fn signature, so a suite can install it directly: + * + * ZTEST_SUITE(my_suite, NULL, NULL, NULL, mod_alloc_test_after, NULL); + * + * Any leaked allocation is freed before the assertion, so one failing test + * case does not cascade into the following ones. + * + * @param fixture ztest suite fixture, unused. + */ +void mod_alloc_test_after(void *fixture); + +#endif /* __TEST_ZTEST_UNIT_COMMON_MOD_ALLOC_H__ */ diff --git a/test/ztest/unit/common/sof_context.c b/test/ztest/unit/common/sof_context.c new file mode 100644 index 000000000000..91d20c644572 --- /dev/null +++ b/test/ztest/unit/common/sof_context.c @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// These contents may have been developed with support from one or more +// Intel-operated generative artificial intelligence solutions. +// +// Minimal SOF context for standalone ztest applications that link SOF code +// calling sof_get(), for example src/audio/component.c, but do not link +// src/init/init.c. The context is created on first use and only its component +// driver list is initialised, which is what buffer_new() and the component +// infrastructure need. + +#include +#include + +#include + +static struct sof sof_context; +static bool sof_context_initialized; + +/** + * @brief Return the minimal SOF context used by ztest unit suites. + * @return Pointer to the singleton test SOF context. + */ +struct sof *sof_get(void) +{ + if (!sof_context_initialized) { + sys_comp_init(&sof_context); + sof_context_initialized = true; + } + + return &sof_context; +} diff --git a/test/ztest/unit/math/fft/CMakeLists.txt b/test/ztest/unit/math/fft/CMakeLists.txt new file mode 100644 index 000000000000..891ea52ba8ca --- /dev/null +++ b/test/ztest/unit/math/fft/CMakeLists.txt @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(sof_unit_math_fft) + +set(SOF_ROOT "${PROJECT_SOURCE_DIR}/../../../../../") +set(sof_top_dir ${SOF_ROOT}) + +include(${SOF_ROOT}/scripts/cmake/misc.cmake) +include(${SOF_ROOT}/scripts/cmake/uuid-registry.cmake) + +target_include_directories(app PRIVATE + ${SOF_ROOT}/zephyr/include + ${SOF_ROOT}/src/include + ${SOF_ROOT}/src/platform/posix/include + ${SOF_ROOT}/src/audio + ${SOF_ROOT}/test/ztest/unit/common + ${SOF_ROOT}/test/cmocka/include + # Reference test vectors are #included in place from the CMocka tree + # rather than copied, so both frameworks share identical data. + ${SOF_ROOT}/test/cmocka/src/math/fft + ${PROJECT_BINARY_DIR}/include/generated +) + +target_compile_definitions(app PRIVATE + -DCONFIG_ZEPHYR_POSIX=1 + -DCONFIG_LIBRARY=1 + -DCONFIG_NUMBERS_NORM=1 + -DUNIT_TEST=1 +) + +target_compile_options(app PRIVATE + -Wno-error=deprecated-declarations +) + +# Keep full ASan/LSan allocation stacks (twister --enable-asan --enable-lsan) +if(CONFIG_ASAN) + target_compile_options(app PRIVATE -fno-omit-frame-pointer) +endif() + +target_sources(app PRIVATE + test_fft_ztest.c + ${SOF_ROOT}/test/ztest/unit/common/mod_alloc.c + ${SOF_ROOT}/test/ztest/unit/common/sof_context.c + ${SOF_ROOT}/zephyr/lib/alloc.c + ${SOF_ROOT}/test/cmocka/src/notifier_mocks.c + ${SOF_ROOT}/src/math/fft/fft_common.c + ${SOF_ROOT}/src/math/fft/fft_16.c + ${SOF_ROOT}/src/math/fft/fft_16_hifi3.c + ${SOF_ROOT}/src/math/fft/fft_32.c + ${SOF_ROOT}/src/math/fft/fft_32_hifi3.c + ${SOF_ROOT}/src/math/numbers.c + ${SOF_ROOT}/src/audio/buffers/comp_buffer.c + ${SOF_ROOT}/src/audio/buffers/audio_buffer.c + ${SOF_ROOT}/src/audio/source_api_helper.c + ${SOF_ROOT}/src/audio/sink_api_helper.c + ${SOF_ROOT}/src/audio/sink_source_utils.c + ${SOF_ROOT}/src/audio/audio_stream.c + ${SOF_ROOT}/src/audio/component.c + ${SOF_ROOT}/src/module/audio/source_api.c + ${SOF_ROOT}/src/module/audio/sink_api.c + ${SOF_ROOT}/src/ipc/ipc3/helper.c + ${SOF_ROOT}/src/ipc/ipc-common.c + ${SOF_ROOT}/src/ipc/ipc-helper.c + ${SOF_ROOT}/src/audio/pipeline/pipeline-graph.c + ${SOF_ROOT}/src/audio/pipeline/pipeline-params.c + ${SOF_ROOT}/src/audio/pipeline/pipeline-schedule.c + ${SOF_ROOT}/src/audio/pipeline/pipeline-stream.c + ${SOF_ROOT}/src/audio/pipeline/pipeline-xrun.c +) + +target_link_libraries(app PRIVATE m) + +sof_append_relative_path_definitions(app) diff --git a/test/ztest/unit/math/fft/prj.conf b/test/ztest/unit/math/fft/prj.conf new file mode 100644 index 000000000000..b92895ae2424 --- /dev/null +++ b/test/ztest/unit/math/fft/prj.conf @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. All rights reserved. + +CONFIG_ZTEST=y +CONFIG_SOF_FULL_ZEPHYR_APPLICATION=n +CONFIG_FORMAT_S16LE=y +CONFIG_FORMAT_S24LE=y +CONFIG_FORMAT_S32LE=y +# Route the SOF heap to the host malloc()/free() of the native simulator, so +# that ASan and LSan can see it. They cannot see blocks carved out of the +# static sys_heap arena that zephyr/lib/alloc.c uses by default. +CONFIG_SOF_NATIVE_SIM_HOST_HEAP=y +# The sys_heap arena is still reserved and initialised by heap_init(), and its +# 256 KiB are too large for SYS_HEAP_SMALL_ONLY (see app/boards/native_sim.conf) +CONFIG_SYS_HEAP_BIG_ONLY=y diff --git a/test/ztest/unit/math/fft/test_fft_ztest.c b/test/ztest/unit/math/fft/test_fft_ztest.c new file mode 100644 index 000000000000..28b9ce33cbf2 --- /dev/null +++ b/test/ztest/unit/math/fft/test_fft_ztest.c @@ -0,0 +1,869 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// These contents may have been developed with support from one or more +// Intel-operated generative artificial intelligence solutions. +// +// Converted from CMock to Ztest +// Original tests from test/cmocka/src/math/fft/fft.c: +// Author: Keyon Jie + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "mod_alloc.h" + +/* Reference test vectors shared with the legacy CMocka test. They are + * #included in place from the CMocka tree (added to the include path in + * CMakeLists.txt) so the exact same fixed-point data is exercised. + */ +#include "input.h" + +/* SNR DB threshold */ +#define FFT_DB_TH_16 27.0 +#define FFT_DB_TH 95.0 +#define SINE_HZ 1000 + +/* For rectangular window frequency matched sine wave test */ +#define TWO_PI 6.28318530717959 +#define SINE_SCALE_S16 32767.0 +#define SINE_SCALE_S32 2147483647.0 +#define SINE_FS 48000.0 +#define SINE_BASE (SINE_FS / 256.0) /* One perid exactly 256 samples, 187.5 Hz */ +#define SINE_FREQ (4.0 * SINE_BASE) /* Four periods, 1125 Hz */ +#define MIN_SNR_256_16 49.0 +#define MIN_SNR_512_16 42.0 +#define MIN_SNR_1024_16 38.0 +#define MIN_SNR_256 132.0 +#define MIN_SNR_512 125.0 +#define MIN_SNR_1024 119.0 + +static struct processing_module dummy; + +/** + * \brief Doing Fast Fourier Transform (FFT) for mono real input buffers. + * \param[in] src - pointer to input buffer. + * \param[out] dst - pointer to output buffer, FFT output for input buffer. + * \param[in] size - input buffer sample count. + */ +static void fft_real(struct comp_buffer *src, struct comp_buffer *dst, uint32_t size) +{ + struct icomplex32 *inb; + struct icomplex32 *outb; + struct fft_plan *plan; + int i; + + if (audio_stream_get_channels(&src->stream) != 1) { + return; + } + + if (src->stream.size < size * sizeof(int32_t) || + dst->stream.size < size * sizeof(struct icomplex32)) { + return; + } + + inb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex32)); + if (!inb) { + return; + } + outb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex32)); + if (!outb) { + goto err_outb; + } + + plan = mod_fft_plan_new(&dummy, inb, outb, size, 32); + if (!plan) { + goto err_plan; + } + + for (i = 0; i < size; i++) { + inb[i].real = *((int32_t *)src->stream.addr + i); + inb[i].imag = 0; + } + + /* perform a single FFT transform */ + fft_execute_32(plan, false); + + for (i = 0; i < size; i++) { + *((int32_t *)dst->stream.addr + 2 * i) = outb[i].real; + *((int32_t *)dst->stream.addr + 2 * i + 1) = outb[i].imag; + } + + mod_fft_plan_free(&dummy, plan); + +err_plan: + rfree(outb); +err_outb: + rfree(inb); +} + +/** + * \brief Doing Inverse Fast Fourier Transform (IFFT) for mono complex input buffers. + * \param[in] src - pointer to input buffer. + * \param[out] dst - pointer to output buffer, FFT output for input buffer. + * \param[in] size - input buffer sample count. + */ +static void ifft_complex(struct comp_buffer *src, struct comp_buffer *dst, uint32_t size) +{ + struct icomplex32 *inb; + struct icomplex32 *outb; + struct fft_plan *plan; + int i; + + if (audio_stream_get_channels(&src->stream) != 1) { + return; + } + + if (src->stream.size < size * sizeof(struct icomplex32) || + dst->stream.size < size * sizeof(struct icomplex32)) { + return; + } + + inb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex32)); + if (!inb) { + return; + } + + outb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex32)); + if (!outb) { + goto err_outb; + } + + plan = mod_fft_plan_new(&dummy, inb, outb, size, 32); + if (!plan) { + goto err_plan; + } + + for (i = 0; i < size; i++) { + inb[i].real = *((int32_t *)src->stream.addr + 2 * i); + inb[i].imag = *((int32_t *)src->stream.addr + 2 * i + 1); + } + + /* perform a single IFFT transform */ + fft_execute_32(plan, true); + + for (i = 0; i < size; i++) { + *((int32_t *)dst->stream.addr + 2 * i) = outb[i].real; + *((int32_t *)dst->stream.addr + 2 * i + 1) = outb[i].imag; + } + + mod_fft_plan_free(&dummy, plan); + +err_plan: + rfree(outb); +err_outb: + rfree(inb); +} + +/** + * \brief Doing Fast Fourier Transform (FFT) for 2 real input buffers. + * \param[in] src - pointer to input buffer (2 channels). + * \param[out] dst1 - pointer to output buffer, FFT output for input buffer 1. + * \param[out] dst2 - pointer to output buffer, FFT output for input buffer 2. + * \param[in] size - input buffer sample count. + */ +static void fft_real_2(struct comp_buffer *src, struct comp_buffer *dst1, struct comp_buffer *dst2, + uint32_t size) +{ + struct icomplex32 *inb; + struct icomplex32 *outb; + struct fft_plan *plan; + int i; + + if (audio_stream_get_channels(&src->stream) != 2) { + return; + } + + if (src->stream.size < size * sizeof(int32_t) * 2 || + dst1->stream.size < size * sizeof(struct icomplex32) || + dst2->stream.size < size * sizeof(struct icomplex32)) { + return; + } + + inb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex32)); + if (!inb) { + return; + } + + outb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex32)); + if (!outb) { + goto err_outb; + } + + plan = mod_fft_plan_new(&dummy, inb, outb, size, 32); + if (!plan) { + goto err_plan; + } + + /* generate complex inputs */ + for (i = 0; i < size; i++) { + inb[i].real = *((int32_t *)src->stream.addr + 2 * i); + inb[i].imag = *((int32_t *)src->stream.addr + 2 * i + 1); + } + + /* perform a single FFT transform */ + fft_execute_32(plan, false); + + /* calculate the outputs */ + *((int32_t *)dst1->stream.addr) = outb[0].real; + *((int32_t *)dst1->stream.addr + 1) = 0; + *((int32_t *)dst2->stream.addr) = 0; + *((int32_t *)dst2->stream.addr + 1) = -outb[0].imag; + for (i = 1; i < size; i++) { + *((int32_t *)dst1->stream.addr + 2 * i) = (outb[i].real + outb[size - i].real) / 2; + *((int32_t *)dst1->stream.addr + 2 * i + 1) = + (outb[i].imag - outb[size - i].imag) / 2; + *((int32_t *)dst2->stream.addr + 2 * i) = (outb[i].imag + outb[size - i].imag) / 2; + *((int32_t *)dst2->stream.addr + 2 * i + 1) = + (outb[size - i].real - outb[i].real) / 2; + } + + mod_fft_plan_free(&dummy, plan); + +err_plan: + rfree(outb); +err_outb: + rfree(inb); +} + +static int power_peak_index_32(struct icomplex32 *out, int fft_size) +{ + int64_t p; + int64_t peak = 0; + int i; + int r = 0; + + /* looking for the peak */ + for (i = 0; i < fft_size / 2; i++) { + p = (int64_t)out[i].real * out[i].real + (int64_t)out[i].imag * out[i].imag; + if (p > peak) { + peak = p; + r = i; + } + } + + return r; +} + +static double integrate_power_32(struct icomplex32 *s, int start_idx, int end_idx) +{ + double energy = 0; + int i; + + for (i = start_idx; i <= end_idx; i++) { + energy += (double)s[i].real * s[i].real + (double)s[i].imag * s[i].imag; + } + + return energy; +} + +static void get_sine_32(int32_t *in, double sine_freq, double fs, int fft_size) +{ + double c = TWO_PI * sine_freq / fs; + double w; + int i; + + for (i = 0; i < fft_size; i++) { + w = c * i; + in[i] = (int32_t)round(SINE_SCALE_S32 * sin(w)); + } +} + +/** + * @brief Verify the 32-bit FFT SNR and peak bin for a 256-point real sine wave + */ +ZTEST(math_fft_suite, test_math_fft_256) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 256 * 2 * sizeof(int32_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex32 *out = (struct icomplex32 *)sink->stream.addr; + int32_t *in = (int32_t *)source->stream.addr; + int fft_size = 256; + int r; + int i; + double signal; + double noise; + double snr; + + /* create sine wave */ + get_sine_32(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real(source, sink, fft_size); + + /* find peak */ + r = power_peak_index_32(out, fft_size); + i = (int)round((SINE_FREQ * fft_size) / SINE_FS); + printf("%s: peak at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* the min. SNR should be met */ + noise = integrate_power_32(out, 0, i - 2); + signal = integrate_power_32(out, i - 1, i + 1); + noise += integrate_power_32(out, i + 2, fft_size / 2 - 1); + snr = 10 * log10(signal / noise); + printf("%s: SNR %5.2f dB\n", __func__, snr); + zassert_true(snr >= MIN_SNR_256, "FFT 256 SNR %5.2f below minimum %5.2f", snr, MIN_SNR_256); + + buffer_free(source); + buffer_free(sink); +} + +/** + * @brief Verify the 32-bit FFT SNR and peak bin for a 512-point real sine wave + */ +ZTEST(math_fft_suite, test_math_fft_512) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 512 * 2 * sizeof(int32_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex32 *out = (struct icomplex32 *)sink->stream.addr; + int32_t *in = (int32_t *)source->stream.addr; + int fft_size = 512; + int r; + int i; + double signal; + double noise; + double snr; + + /* create sine wave */ + get_sine_32(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real(source, sink, fft_size); + + /* find peak */ + r = power_peak_index_32(out, fft_size); + i = (int)round((SINE_FREQ * fft_size) / SINE_FS); + printf("%s: peak at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* the min. SNR should be met */ + noise = integrate_power_32(out, 0, i - 2); + signal = integrate_power_32(out, i - 1, i + 1); + noise += integrate_power_32(out, i + 2, fft_size / 2 - 1); + snr = 10 * log10(signal / noise); + printf("%s: SNR %5.2f dB\n", __func__, snr); + zassert_true(snr >= MIN_SNR_512, "FFT 512 SNR %5.2f below minimum %5.2f", snr, MIN_SNR_512); + + buffer_free(source); + buffer_free(sink); +} + +/** + * @brief Verify the 32-bit FFT SNR and peak bin for a 1024-point real sine wave + */ +ZTEST(math_fft_suite, test_math_fft_1024) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 1024 * 2 * sizeof(int32_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex32 *out = (struct icomplex32 *)sink->stream.addr; + int32_t *in = (int32_t *)source->stream.addr; + int fft_size = 1024; + int r; + int i; + double signal; + double noise; + double snr; + + /* create sine wave */ + get_sine_32(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real(source, sink, fft_size); + + /* find peak */ + r = power_peak_index_32(out, fft_size); + i = (int)round((SINE_FREQ * fft_size) / SINE_FS); + printf("%s: peak at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* the min. SNR should be met */ + noise = integrate_power_32(out, 0, i - 2); + signal = integrate_power_32(out, i - 1, i + 1); + noise += integrate_power_32(out, i + 2, fft_size / 2 - 1); + snr = 10 * log10(signal / noise); + printf("%s: SNR %5.2f dB\n", __func__, snr); + zassert_true(snr >= MIN_SNR_1024, "FFT 1024 SNR %5.2f below minimum %5.2f", snr, + MIN_SNR_1024); + + buffer_free(source); + buffer_free(sink); +} + +/** + * @brief Verify a 1024-point FFT followed by an IFFT reconstructs the signal + */ +ZTEST(math_fft_suite, test_math_fft_1024_ifft) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 1024 * 4 * 2, + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *intm = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex32 *out = (struct icomplex32 *)sink->stream.addr; + double db; + int64_t signal = 0; + int64_t noise = 0; + uint32_t fft_size = 1024; + int32_t *in = (int32_t *)source->stream.addr; + int i; + + get_sine_32(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real(source, intm, fft_size); + + audio_stream_set_channels(&intm->stream, 1); + /* do ifft transform */ + ifft_complex(intm, sink, fft_size); + + /* calculate signal and noise */ + for (i = 0; i < fft_size; i++) { + signal += (int64_t)(in[i] / 32) * (in[i] / 32); + noise += (int64_t)((out[i].real - in[i]) / 32) * ((out[i].real - in[i]) / 32) + + (int64_t)(out[i].imag / 32) * (out[i].imag / 32); + } + + db = 10 * log10((double)signal / noise); + printf("%s: SNR: %6.2f dB\n", __func__, db); + zassert_true(db >= FFT_DB_TH, "FFT/IFFT SNR %6.2f below minimum %6.2f", db, FFT_DB_TH); + + buffer_free(source); + buffer_free(intm); + buffer_free(sink); +} + +/** + * @brief Verify a 512-point 2-channel real FFT finds the expected peak on + * both channels + */ +ZTEST(math_fft_suite, test_math_fft_512_2ch) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 512 * 4 * 2, + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink1 = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink2 = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex32 *out1 = (struct icomplex32 *)sink1->stream.addr; + struct icomplex32 *out2 = (struct icomplex32 *)sink2->stream.addr; + uint32_t fft_size = 512; + int i; + int r; + + for (i = 0; i < fft_size; i++) { + *((int32_t *)source->stream.addr + 2 * i) = input_samples[i]; + *((int32_t *)source->stream.addr + 2 * i + 1) = input_samples[fft_size + i]; + } + + audio_stream_set_channels(&source->stream, 2); + /* do fft transform */ + fft_real_2(source, sink1, sink2, fft_size); + + /* looking for the peak for channel 0 */ + r = power_peak_index_32(out1, fft_size); + i = (SINE_HZ * fft_size) / 48000; + printf("%s: peak for channel 0 at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* looking for the peak for channel 1 */ + r = power_peak_index_32(out2, fft_size); + i = (SINE_HZ * fft_size) / 48000; + printf("%s: peak for channel 1 at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + buffer_free(source); + buffer_free(sink1); + buffer_free(sink2); +} + +/** + * \brief Doing Fast Fourier Transform (FFT) for mono real input buffers. + * \param[in] src - pointer to input buffer. + * \param[out] dst - pointer to output buffer, FFT output for input buffer. + * \param[in] size - input buffer sample count. + */ +static void fft_real_16(struct comp_buffer *src, struct comp_buffer *dst, uint32_t size) +{ + struct icomplex16 *inb; + struct icomplex16 *outb; + struct fft_plan *plan; + int i; + + if (audio_stream_get_channels(&src->stream) != 1) { + return; + } + + if (src->stream.size < size * sizeof(int16_t) || + dst->stream.size < size * sizeof(struct icomplex16)) { + return; + } + + inb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex16)); + if (!inb) { + return; + } + outb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex16)); + if (!outb) { + goto err_outb; + } + + plan = mod_fft_plan_new(&dummy, inb, outb, size, 16); + if (!plan) { + goto err_plan; + } + + for (i = 0; i < size; i++) { + inb[i].real = *((int16_t *)src->stream.addr + i); + inb[i].imag = 0; + } + + /* perform a single FFT transform */ + fft_execute_16(plan, false); + + for (i = 0; i < size; i++) { + *((int16_t *)dst->stream.addr + 2 * i) = outb[i].real; + *((int16_t *)dst->stream.addr + 2 * i + 1) = outb[i].imag; + } + + mod_fft_plan_free(&dummy, plan); + +err_plan: + rfree(outb); +err_outb: + rfree(inb); +} + +/** + * \brief Doing Inverse Fast Fourier Transform (IFFT) for mono complex input buffers. + * \param[in] src - pointer to input buffer. + * \param[out] dst - pointer to output buffer, FFT output for input buffer. + * \param[in] size - input buffer sample count. + */ +static void ifft_complex_16(struct comp_buffer *src, struct comp_buffer *dst, uint32_t size) +{ + struct icomplex16 *inb; + struct icomplex16 *outb; + struct fft_plan *plan; + int i; + + if (audio_stream_get_channels(&src->stream) != 1) { + return; + } + + if (src->stream.size < size * sizeof(struct icomplex16) || + dst->stream.size < size * sizeof(struct icomplex16)) { + return; + } + + inb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex16)); + if (!inb) { + return; + } + + outb = rzalloc(SOF_MEM_FLAG_USER, size * sizeof(struct icomplex16)); + if (!outb) { + goto err_outb; + } + + plan = mod_fft_plan_new(&dummy, inb, outb, size, 16); + if (!plan) { + goto err_plan; + } + + for (i = 0; i < size; i++) { + inb[i].real = *((int16_t *)src->stream.addr + 2 * i); + inb[i].imag = *((int16_t *)src->stream.addr + 2 * i + 1); + } + + /* perform a single IFFT transform */ + fft_execute_16(plan, true); + + for (i = 0; i < size; i++) { + *((int16_t *)dst->stream.addr + 2 * i) = outb[i].real; + *((int16_t *)dst->stream.addr + 2 * i + 1) = outb[i].imag; + } + + mod_fft_plan_free(&dummy, plan); + +err_plan: + rfree(outb); +err_outb: + rfree(inb); +} + +static int power_peak_index_16(struct icomplex16 *out, int fft_size) +{ + int32_t p; + int32_t peak = 0; + int i; + int r = 0; + + /* looking for the peak */ + for (i = 0; i < fft_size / 2; i++) { + p = (int32_t)out[i].real * out[i].real + (int32_t)out[i].imag * out[i].imag; + if (p > peak) { + peak = p; + r = i; + } + } + + return r; +} + +static double integrate_power_16(struct icomplex16 *s, int start_idx, int end_idx) +{ + double energy = 0; + int i; + + for (i = start_idx; i <= end_idx; i++) { + energy += (double)s[i].real * s[i].real + (double)s[i].imag * s[i].imag; + } + + return energy; +} + +static void get_sine_16(int16_t *in, double sine_freq, double fs, int fft_size) +{ + double c = TWO_PI * sine_freq / fs; + double w; + int i; + + for (i = 0; i < fft_size; i++) { + w = c * i; + in[i] = (int16_t)round(SINE_SCALE_S16 * sin(w)); + } +} + +/** + * @brief Verify the 16-bit FFT SNR and peak bin for a 256-point real sine wave + */ +ZTEST(math_fft_suite, test_math_fft_256_16) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 256 * 2 * sizeof(int16_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex16 *out = (struct icomplex16 *)sink->stream.addr; + int16_t *in = (int16_t *)source->stream.addr; + int fft_size = 256; + int r; + int i; + double signal; + double noise; + double snr; + + /* create sine wave */ + get_sine_16(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real_16(source, sink, fft_size); + + /* find peak */ + r = power_peak_index_16(out, fft_size); + i = (int)round((SINE_FREQ * fft_size) / SINE_FS); + printf("%s: peak at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* the min. SNR should be met */ + noise = integrate_power_16(out, 0, i - 2); + signal = integrate_power_16(out, i - 1, i + 1); + noise += integrate_power_16(out, i + 2, fft_size / 2 - 1); + snr = 10 * log10(signal / noise); + printf("%s: SNR %5.2f dB\n", __func__, snr); + zassert_true(snr >= MIN_SNR_256_16, "FFT16 256 SNR %5.2f below minimum %5.2f", snr, + MIN_SNR_256_16); + + buffer_free(source); + buffer_free(sink); +} + +/** + * @brief Verify the 16-bit FFT SNR and peak bin for a 512-point real sine wave + */ +ZTEST(math_fft_suite, test_math_fft_512_16) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 512 * 2 * sizeof(int16_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex16 *out = (struct icomplex16 *)sink->stream.addr; + int16_t *in = (int16_t *)source->stream.addr; + int fft_size = 512; + int r; + int i; + double signal; + double noise; + double snr; + + /* create sine wave */ + get_sine_16(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real_16(source, sink, fft_size); + + /* find peak */ + r = power_peak_index_16(out, fft_size); + i = (int)round((SINE_FREQ * fft_size) / SINE_FS); + printf("%s: peak at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* the min. SNR should be met */ + noise = integrate_power_16(out, 0, i - 2); + signal = integrate_power_16(out, i - 1, i + 1); + noise += integrate_power_16(out, i + 2, fft_size / 2 - 1); + snr = 10 * log10(signal / noise); + printf("%s: SNR %5.2f dB\n", __func__, snr); + zassert_true(snr >= MIN_SNR_512_16, "FFT16 512 SNR %5.2f below minimum %5.2f", snr, + MIN_SNR_512_16); + + buffer_free(source); + buffer_free(sink); +} + +/** + * @brief Verify the 16-bit FFT SNR and peak bin for a 1024-point real sine wave + */ +ZTEST(math_fft_suite, test_math_fft_1024_16) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 1024 * 2 * sizeof(int16_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex16 *out = (struct icomplex16 *)sink->stream.addr; + int16_t *in = (int16_t *)source->stream.addr; + int fft_size = 1024; + int r; + int i; + double signal; + double noise; + double snr; + + /* create sine wave */ + get_sine_16(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real_16(source, sink, fft_size); + + /* find peak */ + r = power_peak_index_16(out, fft_size); + i = (int)round((SINE_FREQ * fft_size) / SINE_FS); + printf("%s: peak at point %d\n", __func__, r); + + /* the peak should be in range i +/-1 */ + zassert_between_inclusive(r, i - 1, i + 1); + + /* the min. SNR should be met */ + noise = integrate_power_16(out, 0, i - 2); + signal = integrate_power_16(out, i - 1, i + 1); + noise += integrate_power_16(out, i + 2, fft_size / 2 - 1); + snr = 10 * log10(signal / noise); + printf("%s: SNR %5.2f dB\n", __func__, snr); + zassert_true(snr >= MIN_SNR_1024_16, "FFT16 1024 SNR %5.2f below minimum %5.2f", snr, + MIN_SNR_1024_16); + + buffer_free(source); + buffer_free(sink); +} + +/** + * @brief Verify a 1024-point 16-bit FFT followed by an IFFT reconstructs the signal + */ +ZTEST(math_fft_suite, test_math_fft_1024_ifft_16) +{ + struct sof_ipc_buffer test_buf_desc = { + .size = 1024 * 2 * sizeof(int16_t), + }; + struct comp_buffer *source = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *intm = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct comp_buffer *sink = buffer_new(NULL, &test_buf_desc, BUFFER_USAGE_NOT_SHARED); + struct icomplex16 *out = (struct icomplex16 *)sink->stream.addr; + double db; + int64_t signal = 0; + int64_t noise = 0; + uint32_t fft_size = 1024; + int16_t *in = (int16_t *)source->stream.addr; + int i; + + get_sine_16(in, SINE_FREQ, SINE_FS, fft_size); + audio_stream_set_channels(&source->stream, 1); + + /* do fft transform */ + fft_real_16(source, intm, fft_size); + + audio_stream_set_channels(&intm->stream, 1); + /* do ifft transform */ + ifft_complex_16(intm, sink, fft_size); + + /* calculate signal and noise */ + for (i = 0; i < fft_size; i++) { + signal += (int64_t)in[i] * in[i]; + noise += (int64_t)(out[i].real - in[i]) * (out[i].real - in[i]) + + (int64_t)out[i].imag * out[i].imag; + } + + db = 10 * log10((double)signal / noise); + printf("%s: SNR: %6.2f dB\n", __func__, db); + zassert_true(db >= FFT_DB_TH_16, "FFT16/IFFT16 SNR %6.2f below minimum %6.2f", db, + FFT_DB_TH_16); + + buffer_free(source); + buffer_free(intm); + buffer_free(sink); +} + +/** + * @brief Define and initialize the FFT/IFFT test suite + * + * mod_alloc_test_after() checks that each test case freed the FFT plans it + * allocated through the mod_alloc() API. + */ +ZTEST_SUITE(math_fft_suite, NULL, NULL, NULL, mod_alloc_test_after, NULL); diff --git a/test/ztest/unit/math/fft/testcase.yaml b/test/ztest/unit/math/fft/testcase.yaml new file mode 100644 index 000000000000..93a7a5a24912 --- /dev/null +++ b/test/ztest/unit/math/fft/testcase.yaml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. All rights reserved. +# +# Math FFT/IFFT unit tests converted from CMock to Ztest +# +# These contents may have been developed with support from one or more Intel-operated +# generative artificial intelligence solutions. +# + +common: + tags: + - SOF + - unit_test + - math + - fft + integration_platforms: + - native_sim + arch_exclude: xtensa + +tests: + sof.unit.math.fft: + platform_allow: + - native_sim + build_only: false diff --git a/test/ztest/unit/math/fft_multi/CMakeLists.txt b/test/ztest/unit/math/fft_multi/CMakeLists.txt new file mode 100644 index 000000000000..b08ee40c0884 --- /dev/null +++ b/test/ztest/unit/math/fft_multi/CMakeLists.txt @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(sof_unit_math_fft_multi) + +set(SOF_ROOT "${PROJECT_SOURCE_DIR}/../../../../../") +set(sof_top_dir ${SOF_ROOT}) + +include(${SOF_ROOT}/scripts/cmake/misc.cmake) +include(${SOF_ROOT}/scripts/cmake/uuid-registry.cmake) + +target_include_directories(app PRIVATE + ${SOF_ROOT}/zephyr/include + ${SOF_ROOT}/src/include + ${SOF_ROOT}/src/platform/posix/include + ${SOF_ROOT}/test/ztest/unit/common + ${SOF_ROOT}/test/cmocka/include + # Reference test vectors are #included in place from the CMocka tree + # rather than copied, so both frameworks share identical data. + ${SOF_ROOT}/test/cmocka/src/math/fft + ${PROJECT_BINARY_DIR}/include/generated +) + +target_compile_definitions(app PRIVATE + -DCONFIG_ZEPHYR_POSIX=1 + -DCONFIG_LIBRARY=1 + -DCONFIG_NUMBERS_NORM=1 + -DUNIT_TEST=1 +) + +target_compile_options(app PRIVATE + -Wno-error=deprecated-declarations +) + +# Keep full ASan/LSan allocation stacks (twister --enable-asan --enable-lsan) +if(CONFIG_ASAN) + target_compile_options(app PRIVATE -fno-omit-frame-pointer) +endif() + +target_sources(app PRIVATE + test_fft_multi_ztest.c + ${SOF_ROOT}/test/ztest/unit/common/mod_alloc.c + ${SOF_ROOT}/zephyr/lib/alloc.c + ${SOF_ROOT}/test/cmocka/src/notifier_mocks.c + ${SOF_ROOT}/src/math/fft/fft_multi.c + ${SOF_ROOT}/src/math/fft/fft_multi_generic.c + ${SOF_ROOT}/src/math/fft/fft_multi_hifi3.c + ${SOF_ROOT}/src/math/fft/fft_common.c + ${SOF_ROOT}/src/math/fft/fft_32.c +) + +target_link_libraries(app PRIVATE m) + +sof_append_relative_path_definitions(app) diff --git a/test/ztest/unit/math/fft_multi/prj.conf b/test/ztest/unit/math/fft_multi/prj.conf new file mode 100644 index 000000000000..06beff4cad2c --- /dev/null +++ b/test/ztest/unit/math/fft_multi/prj.conf @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. All rights reserved. + +CONFIG_ZTEST=y +CONFIG_SOF_FULL_ZEPHYR_APPLICATION=n +# Route the SOF heap to the host malloc()/free() of the native simulator, so +# that ASan and LSan can see it. They cannot see blocks carved out of the +# static sys_heap arena that zephyr/lib/alloc.c uses by default. +CONFIG_SOF_NATIVE_SIM_HOST_HEAP=y +# The sys_heap arena is still reserved and initialised by heap_init(), and its +# 256 KiB are too large for SYS_HEAP_SMALL_ONLY (see app/boards/native_sim.conf) +CONFIG_SYS_HEAP_BIG_ONLY=y diff --git a/test/ztest/unit/math/fft_multi/test_fft_multi_ztest.c b/test/ztest/unit/math/fft_multi/test_fft_multi_ztest.c new file mode 100644 index 000000000000..2df55b4480ad --- /dev/null +++ b/test/ztest/unit/math/fft_multi/test_fft_multi_ztest.c @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright(c) 2026 Intel Corporation. +// +// These contents may have been developed with support from one or more +// Intel-operated generative artificial intelligence solutions. +// +// Converted from CMock to Ztest +// Original tests from test/cmocka/src/math/fft/fft_multi.c: +// Author: Seppo Ingalsuo + +#include + +#include +#include +#include +#include +#include +#include + +#include "mod_alloc.h" + +/* Reference test vectors shared with the legacy CMocka test. They are + * #included in place from the CMocka tree (added to the include path in + * CMakeLists.txt) so the exact same fixed-point data is exercised. + */ +#include "ref_fft_multi_96_32.h" +#include "ref_fft_multi_512_32.h" +#include "ref_fft_multi_768_32.h" +#include "ref_fft_multi_1024_32.h" +#include "ref_fft_multi_1536_32.h" +#include "ref_fft_multi_3072_32.h" + +#include "ref_ifft_multi_24_32.h" +#include "ref_ifft_multi_256_32.h" +#include "ref_ifft_multi_1024_32.h" +#include "ref_ifft_multi_1536_32.h" +#include "ref_ifft_multi_3072_32.h" + +#include +#include +#include +#include +#include +#include +#include + +#define FFT_MAX_ERROR_ABS 1050.0 /* about -126 dB */ +#define FFT_MAX_ERROR_RMS 35.0 /* about -156 dB */ +#define IFFT_MAX_ERROR_ABS 2400000.0 /* about -59 dB */ +#define IFFT_MAX_ERROR_RMS 44000.0 /* about -94 dB */ + +static struct processing_module dummy; + +/** + * @brief Run a multi-size FFT/IFFT test against a reference vector. + * + * Feeds each of the num_tests blocks of num_bins complex samples through + * the FFT-multi plan and accumulates the absolute and RMS error against + * the pre-computed reference, preserving the fixed-point semantics of the + * original CMocka test (the debug .txt file dumps of the legacy test are + * dropped as they were unused diagnostics, not part of the test itself). + */ +static void fft_multi_32_test(const int32_t *in_real, const int32_t *in_imag, + const int32_t *ref_real, const int32_t *ref_imag, int num_bins, + int num_tests, double max_error_abs, double max_error_rms, + bool do_ifft) +{ + struct icomplex32 *x; + struct icomplex32 *y; + struct fft_multi_plan *plan; + double delta; + double error_rms; + double delta_max = 0; + double sum_squares = 0; + const int32_t *p_in_real = in_real; + const int32_t *p_in_imag = in_imag; + const int32_t *p_ref_real = ref_real; + const int32_t *p_ref_imag = ref_imag; + int i, j; + + x = rzalloc(SOF_MEM_FLAG_USER, num_bins * sizeof(struct icomplex32)); + zassert_not_null(x, "Failed to allocate input data buffer."); + + y = rzalloc(SOF_MEM_FLAG_USER, num_bins * sizeof(struct icomplex32)); + zassert_not_null(y, "Failed to allocate output data buffer."); + + plan = mod_fft_multi_plan_new(&dummy, x, y, num_bins, 32); + zassert_not_null(plan, "Failed to allocate FFT plan."); + + for (i = 0; i < num_tests; i++) { + for (j = 0; j < num_bins; j++) { + x[j].real = *p_in_real++; + x[j].imag = *p_in_imag++; + } + + fft_multi_execute_32(plan, do_ifft); + + for (j = 0; j < num_bins; j++) { + delta = (double)*p_ref_real - (double)y[j].real; + sum_squares += delta * delta; + if (delta > delta_max) { + delta_max = delta; + } else if (-delta > delta_max) { + delta_max = -delta; + } + + delta = (double)*p_ref_imag - (double)y[j].imag; + sum_squares += delta * delta; + if (delta > delta_max) { + delta_max = delta; + } else if (-delta > delta_max) { + delta_max = -delta; + } + + p_ref_real++; + p_ref_imag++; + } + } + + mod_fft_multi_plan_free(&dummy, plan); + rfree(y); + rfree(x); + + error_rms = sqrt(sum_squares / (double)(2 * num_bins * num_tests)); + printf("Max absolute error = %5.2f (limit %5.2f), error RMS = %5.2f (limit %5.2f)\n", + delta_max, max_error_abs, error_rms, max_error_rms); + + zassert_true(error_rms < max_error_rms, "error RMS %5.2f exceeds limit %5.2f", error_rms, + max_error_rms); + zassert_true(delta_max < max_error_abs, "Max absolute error %5.2f exceeds limit %5.2f", + delta_max, max_error_abs); +} + +/** + * @brief Verify a 96-bin FFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_fft_multi_96) +{ + fft_multi_32_test(fft_in_real_96_q31, fft_in_imag_96_q31, fft_ref_real_96_q31, + fft_ref_imag_96_q31, 96, REF_SOFM_FFT_MULTI_96_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); +} + +/** + * @brief Verify a 512-bin FFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_fft_multi_512) +{ + fft_multi_32_test(fft_in_real_512_q31, fft_in_imag_512_q31, fft_ref_real_512_q31, + fft_ref_imag_512_q31, 512, REF_SOFM_FFT_MULTI_512_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); +} + +/** + * @brief Verify a 768-bin FFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_fft_multi_768) +{ + fft_multi_32_test(fft_in_real_768_q31, fft_in_imag_768_q31, fft_ref_real_768_q31, + fft_ref_imag_768_q31, 768, REF_SOFM_FFT_MULTI_768_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); +} + +/** + * @brief Verify a 1024-bin FFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_fft_multi_1024) +{ + fft_multi_32_test(fft_in_real_1024_q31, fft_in_imag_1024_q31, fft_ref_real_1024_q31, + fft_ref_imag_1024_q31, 1024, REF_SOFM_FFT_MULTI_1024_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); +} + +/** + * @brief Verify a 1536-bin FFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_fft_multi_1536) +{ + fft_multi_32_test(fft_in_real_1536_q31, fft_in_imag_1536_q31, fft_ref_real_1536_q31, + fft_ref_imag_1536_q31, 1536, REF_SOFM_FFT_MULTI_1536_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); +} + +/** + * @brief Verify a 3072-bin FFT against its reference vector, twice, matching + * the two invocations exercised by the original CMocka test. + */ +ZTEST(math_fft_multi_suite, test_fft_multi_3072) +{ + fft_multi_32_test(fft_in_real_3072_q31, fft_in_imag_3072_q31, fft_ref_real_3072_q31, + fft_ref_imag_3072_q31, 3072, REF_SOFM_FFT_MULTI_3072_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); + fft_multi_32_test(fft_in_real_3072_q31, fft_in_imag_3072_q31, fft_ref_real_3072_q31, + fft_ref_imag_3072_q31, 3072, REF_SOFM_FFT_MULTI_3072_NUM_TESTS, + FFT_MAX_ERROR_ABS, FFT_MAX_ERROR_RMS, false); +} + +/** + * @brief Verify a 24-bin IFFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_ifft_multi_24) +{ + fft_multi_32_test(ifft_in_real_24_q31, ifft_in_imag_24_q31, ifft_ref_real_24_q31, + ifft_ref_imag_24_q31, 24, REF_SOFM_IFFT_MULTI_24_NUM_TESTS, + IFFT_MAX_ERROR_ABS, IFFT_MAX_ERROR_RMS, true); +} + +/** + * @brief Verify a 256-bin IFFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_ifft_multi_256) +{ + fft_multi_32_test(ifft_in_real_256_q31, ifft_in_imag_256_q31, ifft_ref_real_256_q31, + ifft_ref_imag_256_q31, 256, REF_SOFM_IFFT_MULTI_256_NUM_TESTS, + IFFT_MAX_ERROR_ABS, IFFT_MAX_ERROR_RMS, true); +} + +/** + * @brief Verify a 1024-bin IFFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_ifft_multi_1024) +{ + fft_multi_32_test(ifft_in_real_1024_q31, ifft_in_imag_1024_q31, ifft_ref_real_1024_q31, + ifft_ref_imag_1024_q31, 1024, REF_SOFM_IFFT_MULTI_1024_NUM_TESTS, + IFFT_MAX_ERROR_ABS, IFFT_MAX_ERROR_RMS, true); +} + +/** + * @brief Verify a 1536-bin IFFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_ifft_multi_1536) +{ + fft_multi_32_test(ifft_in_real_1536_q31, ifft_in_imag_1536_q31, ifft_ref_real_1536_q31, + ifft_ref_imag_1536_q31, 1536, REF_SOFM_IFFT_MULTI_1536_NUM_TESTS, + IFFT_MAX_ERROR_ABS, IFFT_MAX_ERROR_RMS, true); +} + +/** + * @brief Verify a 3072-bin IFFT against its reference vector + */ +ZTEST(math_fft_multi_suite, test_ifft_multi_3072) +{ + fft_multi_32_test(ifft_in_real_3072_q31, ifft_in_imag_3072_q31, ifft_ref_real_3072_q31, + ifft_ref_imag_3072_q31, 3072, REF_SOFM_IFFT_MULTI_3072_NUM_TESTS, + IFFT_MAX_ERROR_ABS, IFFT_MAX_ERROR_RMS, true); +} + +/** + * @brief Define and initialize the FFT-multi test suite + * + * mod_alloc_test_after() checks that each test case freed the FFT plans it + * allocated through the mod_alloc() API. + */ +ZTEST_SUITE(math_fft_multi_suite, NULL, NULL, NULL, mod_alloc_test_after, NULL); diff --git a/test/ztest/unit/math/fft_multi/testcase.yaml b/test/ztest/unit/math/fft_multi/testcase.yaml new file mode 100644 index 000000000000..15427c35de97 --- /dev/null +++ b/test/ztest/unit/math/fft_multi/testcase.yaml @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: BSD-3-Clause +# +# Copyright(c) 2026 Intel Corporation. All rights reserved. +# +# Math multi-size FFT/IFFT unit tests converted from CMock to Ztest +# +# These contents may have been developed with support from one or more Intel-operated +# generative artificial intelligence solutions. +# + +common: + tags: + - SOF + - unit_test + - math + - fft + integration_platforms: + - native_sim + arch_exclude: xtensa + +tests: + sof.unit.math.fft_multi: + platform_allow: + - native_sim + build_only: false diff --git a/test/ztest/unit/objpool/test_objpool_ztest.c b/test/ztest/unit/objpool/test_objpool_ztest.c index 596fb3accd70..6bf7dba0aa78 100644 --- a/test/ztest/unit/objpool/test_objpool_ztest.c +++ b/test/ztest/unit/objpool/test_objpool_ztest.c @@ -44,6 +44,8 @@ ZTEST(objpool_suite, test_objpool_wrong_size) zassert_ok(objpool_free(&head, block3)); zassert_not_ok(objpool_free(&head, block4 + 1)); zassert_ok(objpool_free(&head, block4)); + + objpool_prune(&head); } ZTEST(objpool_suite, test_objpool) @@ -73,6 +75,8 @@ ZTEST(objpool_suite, test_objpool) while (k--) zassert_ok(objpool_free(&head, blocks[k]), "free failed"); + + objpool_prune(&head); } struct test_objpool_data { @@ -113,6 +117,8 @@ ZTEST(objpool_suite, test_objpool_iterate) /* Reset for a possible rerun */ test_objpool_check = 0; + + objpool_prune(&head); } ZTEST_SUITE(objpool_suite, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/lib/alloc.c b/zephyr/lib/alloc.c index f42270efe95b..89bd49286831 100644 --- a/zephyr/lib/alloc.c +++ b/zephyr/lib/alloc.c @@ -392,10 +392,13 @@ SYS_INIT(virtual_heap_init, POST_KERNEL, 1); #endif /* CONFIG_VIRTUAL_HEAP */ +#if CONFIG_SOF_FULL_ZEPHYR_APPLICATION +/* unit-test builds get a static inline NULL stub from rtos/alloc.h */ struct k_heap *sof_sys_heap_get(void) { return &sof_heap; } +#endif struct k_heap *sof_sys_user_heap_get(void) {