From 5677761614642bdde67ebaf96054410e31983fbc Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 2 Feb 2026 11:19:54 +0000 Subject: [PATCH 01/10] started putting up a framework for thew new aec_unit tests --- .../lib_aec/aec_unit_tests_new/CMakeLists.txt | 58 ++++++++++++++ tests/lib_aec/aec_unit_tests_new/conftest.py | 50 ++++++++++++ .../src/test_calc_coherence.c | 53 +++++++++++++ .../aec_unit_tests_new/test_calc_coherence.py | 79 +++++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 tests/lib_aec/aec_unit_tests_new/CMakeLists.txt create mode 100644 tests/lib_aec/aec_unit_tests_new/conftest.py create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py diff --git a/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt b/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt new file mode 100644 index 00000000..dad438aa --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt @@ -0,0 +1,58 @@ + +cmake_minimum_required(VERSION 3.21) +include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake) +project(aec_unit_tests_new) + +set(APP_HW_TARGET XK-EVK-XU316) +set(XMOS_SANDBOX_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../../) +set(CONFIG_XSCOPE_PATH ${XMOS_SANDBOX_DIR}/lib_voice/tests/shared/file_utils) + +file(GLOB CONFIG_XSCOPE_REL_PATH + RELATIVE ${CMAKE_CURRENT_LIST_DIR} + CONFIGURE_DEPENDS + ${CONFIG_XSCOPE_PATH}/config.xscope) + +file(GLOB APP_C_SRCS + RELATIVE ${CMAKE_CURRENT_LIST_DIR} + CONFIGURE_DEPENDS + ${XMOS_SANDBOX_DIR}/lib_voice/tests/lib_vnr/vnr_unit_tests/src/main.c + ${XMOS_SANDBOX_DIR}/lib_voice/tests/shared/file_utils/src/*.c + ${CMAKE_CURRENT_LIST_DIR}/src/*.c) + +file(GLOB APP_XC_SRCS + RELATIVE ${CMAKE_CURRENT_LIST_DIR} + CONFIGURE_DEPENDS + ${XMOS_SANDBOX_DIR}/lib_voice/tests/lib_vnr/vnr_unit_tests/src/main.xc) + +set(APP_INCLUDES + ${CMAKE_CURRENT_LIST_DIR}/src + ${CMAKE_CURRENT_LIST_DIR}/../../shared/file_utils/src) + +file(GLOB_RECURSE tests RELATIVE ${CMAKE_CURRENT_LIST_DIR} CONFIGURE_DEPENDS src/test*.c) + +foreach(test_file ${tests}) + get_filename_component(test_name ${test_file} NAME_WE) + set(SOURCE_FILES_${test_name} ${test_file}) + if (NOT BUILD_NATIVE) + set(APP_DEPENDENT_MODULES "lib_voice" + "xscope_fileio") + set(APP_COMPILER_FLAGS_${test_name} + -report + -Os + -g + -Wno-xcore-fptrgroup + -DSPEEDUP_FACTOR=${TEST_SPEEDUP_FACTOR} + -DTEST_WAV_XSCOPE=1) + + set(APP_XSCOPE_SRCS ${CONFIG_XSCOPE_REL_PATH}) + else() + set(APP_DEPENDENT_MODULES "lib_voice") + + set(APP_COMPILER_FLAGS_${test_name} + -Os + -DX86_BUILD=1) + endif() +endforeach() + +XMOS_REGISTER_APP() + diff --git a/tests/lib_aec/aec_unit_tests_new/conftest.py b/tests/lib_aec/aec_unit_tests_new/conftest.py new file mode 100644 index 00000000..193614d2 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/conftest.py @@ -0,0 +1,50 @@ +# Copyright 2022-2026 XMOS LIMITED. +# This Software is subject to the terms of the XMOS Public Licence: Version 1. +import pytest +from pathlib import Path +import py_voice.modules.aec as aec +import py_voice.config.config as pv_config +import py_voice +import numpy as np +from run_dut import run_dut + +PY_VOICE_ROOT = Path(py_voice.__file__).resolve().parent +default_conf_path = PY_VOICE_ROOT / "config" / "defaults.json" +default_conf = pv_config.get_config_dict(default_conf_path) +bin_dir_path = Path(__file__).parent / "bin" + +@pytest.fixture +def aec_obj(y_ch, x_ch, main_ph, shadow_ph): + test_conf = default_conf + test_conf["general"]["modules"] = ["aec"] + test_conf["general"]["input_channel_count"] = y_ch + x_ch + test_conf["general"]["output_channel_count"] = y_ch + test_conf["aec"]["input_channel_count"] = y_ch + x_ch + test_conf["aec"]["output_channel_count"] = y_ch + test_conf["aec"]["y_channel_count"] = y_ch + test_conf["aec"]["x_channel_count"] = x_ch + test_conf["aec"]["phases"] = main_ph + test_conf["aec"]["phases_shadow"] = shadow_ph + + return aec.aec(test_conf) + +@pytest.fixture +def dut_runner(request, target): + exe_path = bin_dir_path / request.node.originalname / f"aec_unit_tests_new_{request.node.originalname}" + + def _run_dut(input_bin): + op, _ = run_dut(input_bin, exe_path, target) + return op + + return _run_dut + +@pytest.fixture +def rng(): + return np.random.default_rng(1243) + +def pytest_generate_tests(metafunc): + if "target" in metafunc.fixturenames: + metafunc.parametrize("target", [ + 'native', + 'xs3a' + ]) diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c new file mode 100644 index 00000000..a35c3efc --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c @@ -0,0 +1,53 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. +#include +#include +#include +#include +#include +#include "aec.h" +#include "aec_priv.h" + +#if BUILD_NATIVE +aec_task_distribution_t tdist = aec_tdist_chans2_threads1; +#else +aec_task_distribution_t tdist = aec_tdist_chans2_threads2; +#endif + +static aec_state_t aec_state; +void test_init() +{ + aec_init(&aec_state, 1, 1, 9, 0, &tdist); +} + +void test(int32_t *output, int32_t *input) +{ + aec_frame_init(&aec_state.main_state, NULL, &input[2], &input[AEC_PROC_FRAME_LENGTH + 2 + 2]); //frame init will copy y[240:480] into output + + bfp_s32_init(&aec_state.main_state.y_hat[0], (int32_t*)&aec_state.main_state.Y_hat[0].data[0], 0, AEC_PROC_FRAME_LENGTH, 0); + + + aec_state.main_state.shared_state->y[0].exp = input[0]; + aec_state.main_state.shared_state->y[0].hr = input[1]; + aec_state.main_state.shared_state->y[0].data = &input[2]; + + aec_state.main_state.y_hat[0].exp = input[AEC_PROC_FRAME_LENGTH + 2]; + aec_state.main_state.y_hat[0].hr = input[AEC_PROC_FRAME_LENGTH + 2 + 1]; + aec_state.main_state.y_hat[0].data = &input[AEC_PROC_FRAME_LENGTH + 2 + 2]; + + // since aec_state.main_state.shared_state->y is being initialised with a new frame after calling aec_frame_init(), + // we need to update aec_state.main_state->shared_state->prev_y again since that's where y[240:480] is read from in aec_calc_coherence() + memcpy(aec_state.main_state.shared_state->prev_y[0].data, + &aec_state.main_state.shared_state->y[0].data[AEC_FRAME_ADVANCE], + (AEC_PROC_FRAME_LENGTH-AEC_FRAME_ADVANCE)*sizeof(int32_t)); + + aec_state.main_state.shared_state->prev_y[0].exp = aec_state.main_state.shared_state->y[0].exp; + aec_state.main_state.shared_state->prev_y[0].hr = aec_state.main_state.shared_state->y[0].hr; + + aec_calc_coherence(&aec_state.main_state, 0); + + coherence_mu_params_t *coh_mu_state_ptr = &aec_state.main_state.shared_state->coh_mu_state[0]; + + memcpy(output, &coh_mu_state_ptr->coh, sizeof(float_s32_t)); + memcpy((int8_t *)output[sizeof(float_s32_t)], &coh_mu_state_ptr->coh_slow, sizeof(float_s32_t)); +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py new file mode 100644 index 00000000..1a34b76d --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py @@ -0,0 +1,79 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc + + +# runtime: +# bypass = rand % 2 + +# y: +# hr = rand % 4 +# exp = -31,32 +# data rand >> hr +# 512 + +# y_hat: same as y + +# out: 2 x float_s32_t +# accuracy: 1<< 13 + +def rand_int32_arr(rng, size=None, hr_max=1, min=np.iinfo(np.int32).min, max=np.iinfo(np.int32).max+1): + hr = rng.integers(hr_max) + data = rng.integers(min, max, size=size, dtype=np.int32) + data >>= hr + return data, hr + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 1, 9, 0]]) +def test_calc_coherence(aec_obj, rng, dut_runner): + proc_fr_len = 512 + + y_len = proc_fr_len + 2 # for hr and exp + y_hat_len = y_len + + out_len = 2 * 2 # 2 x float_s32_t + in_len = y_len + y_hat_len + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 10 + ref_coh = np.empty(0, dtype=np.float64) + ref_coh_slow = np.empty(0, dtype=np.float64) + + for _ in range(test_frames): + y, y_hr = rand_int32_arr(rng, proc_fr_len, 4) + y_exp = rng.integers(-31, 32, size=1, dtype=np.int32) + + input_data = np.append(input_data, y_exp) + input_data = np.append(input_data, y_hr) + input_data = np.append(input_data, y) + + y_hat, y_hat_hr = rand_int32_arr(rng, proc_fr_len, 4) + y_hat_exp = rng.integers(-31, 32, size=1, dtype=np.int32) + + input_data = np.append(input_data, y_hat_exp) + input_data = np.append(input_data, y_hat_hr) + input_data = np.append(input_data, y_hat) + + y_fl = pvc.int32_to_double(y, y_exp) + y_hat_fl = pvc.int32_to_double(y_hat, y_hat_exp) + + aec_obj.y_data[0] = y_fl + aec_obj.y_hat[0] = y_hat + aec_obj.calc_coherence() + + ref_coh = np.append(ref_coh, aec_obj.coh) + ref_coh_slow = np.append(ref_coh_slow, aec_obj.coh_slow) + + op = dut_runner(input_data) + + sections = np.cumsum(np.tile([2, 2], test_frames))[:-1].astype(np.int32) + op_split = np.split(op.sections) + + print(op_split) + + # dut_coh = np.concatenate(op_split[0::2]) + # dut_coh_slow = np.concatenate(op_split[1::2]) + + print(ref_coh) + print(ref_coh_slow) + From 111e4fe2a267178591df9b2e2f05cdfbe1df2972 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 2 Feb 2026 12:35:48 +0000 Subject: [PATCH 02/10] fixed load store --- tests/lib_aec/aec_unit_tests_new/CMakeLists.txt | 1 + .../aec_unit_tests_new/src/test_calc_coherence.c | 14 +++++++------- .../aec_unit_tests_new/test_calc_coherence.py | 12 +++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt b/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt index dad438aa..893f2fca 100644 --- a/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt +++ b/tests/lib_aec/aec_unit_tests_new/CMakeLists.txt @@ -50,6 +50,7 @@ foreach(test_file ${tests}) set(APP_COMPILER_FLAGS_${test_name} -Os + -DBUILD_NATIVE=1 -DX86_BUILD=1) endif() endforeach() diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c index a35c3efc..a58fd9fe 100644 --- a/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c @@ -8,15 +8,15 @@ #include "aec.h" #include "aec_priv.h" -#if BUILD_NATIVE -aec_task_distribution_t tdist = aec_tdist_chans2_threads1; -#else -aec_task_distribution_t tdist = aec_tdist_chans2_threads2; -#endif - static aec_state_t aec_state; void test_init() { + #if BUILD_NATIVE + aec_task_distribution_t tdist = aec_tdist_chans2_threads1; + #else + aec_task_distribution_t tdist = aec_tdist_chans2_threads2; + #endif + aec_init(&aec_state, 1, 1, 9, 0, &tdist); } @@ -49,5 +49,5 @@ void test(int32_t *output, int32_t *input) coherence_mu_params_t *coh_mu_state_ptr = &aec_state.main_state.shared_state->coh_mu_state[0]; memcpy(output, &coh_mu_state_ptr->coh, sizeof(float_s32_t)); - memcpy((int8_t *)output[sizeof(float_s32_t)], &coh_mu_state_ptr->coh_slow, sizeof(float_s32_t)); + memcpy((int8_t *)output + sizeof(float_s32_t), &coh_mu_state_ptr->coh_slow, sizeof(float_s32_t)); } diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py index 1a34b76d..510c7185 100644 --- a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py @@ -67,13 +67,15 @@ def test_calc_coherence(aec_obj, rng, dut_runner): op = dut_runner(input_data) sections = np.cumsum(np.tile([2, 2], test_frames))[:-1].astype(np.int32) - op_split = np.split(op.sections) + op_split = np.split(op, sections) - print(op_split) - - # dut_coh = np.concatenate(op_split[0::2]) - # dut_coh_slow = np.concatenate(op_split[1::2]) + dut_coh = np.concatenate(op_split[0::2]) + dut_coh = pvc.float_s32_arr_to_double(dut_coh) + dut_coh_slow = np.concatenate(op_split[1::2]) + dut_coh_slow = pvc.float_s32_arr_to_double(dut_coh_slow) print(ref_coh) + print(dut_coh) print(ref_coh_slow) + print(dut_coh_slow) From c847eb10e8525523f94bb10ab58c56dc9c9298ab Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 3 Feb 2026 14:40:23 +0000 Subject: [PATCH 03/10] fixed coherence test --- .../src/test_calc_coherence.c | 25 +++--------- .../aec_unit_tests_new/test_calc_coherence.py | 39 ++++++------------- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c index a58fd9fe..aa07980d 100644 --- a/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_coherence.c @@ -18,31 +18,18 @@ void test_init() #endif aec_init(&aec_state, 1, 1, 9, 0, &tdist); + aec_state.main_state.shared_state->ref_active_flag = 1; } void test(int32_t *output, int32_t *input) { - aec_frame_init(&aec_state.main_state, NULL, &input[2], &input[AEC_PROC_FRAME_LENGTH + 2 + 2]); //frame init will copy y[240:480] into output + // test_calc_coherence is interested in y_hat[240:480] and prev_y[0:240] + aec_state.main_state.shared_state->prev_y[0].data = &input[1]; + aec_state.main_state.shared_state->prev_y[0].exp = input[0]; bfp_s32_init(&aec_state.main_state.y_hat[0], (int32_t*)&aec_state.main_state.Y_hat[0].data[0], 0, AEC_PROC_FRAME_LENGTH, 0); - - - aec_state.main_state.shared_state->y[0].exp = input[0]; - aec_state.main_state.shared_state->y[0].hr = input[1]; - aec_state.main_state.shared_state->y[0].data = &input[2]; - - aec_state.main_state.y_hat[0].exp = input[AEC_PROC_FRAME_LENGTH + 2]; - aec_state.main_state.y_hat[0].hr = input[AEC_PROC_FRAME_LENGTH + 2 + 1]; - aec_state.main_state.y_hat[0].data = &input[AEC_PROC_FRAME_LENGTH + 2 + 2]; - - // since aec_state.main_state.shared_state->y is being initialised with a new frame after calling aec_frame_init(), - // we need to update aec_state.main_state->shared_state->prev_y again since that's where y[240:480] is read from in aec_calc_coherence() - memcpy(aec_state.main_state.shared_state->prev_y[0].data, - &aec_state.main_state.shared_state->y[0].data[AEC_FRAME_ADVANCE], - (AEC_PROC_FRAME_LENGTH-AEC_FRAME_ADVANCE)*sizeof(int32_t)); - - aec_state.main_state.shared_state->prev_y[0].exp = aec_state.main_state.shared_state->y[0].exp; - aec_state.main_state.shared_state->prev_y[0].hr = aec_state.main_state.shared_state->y[0].hr; + memcpy(&aec_state.main_state.y_hat[0].data[AEC_FRAME_ADVANCE], &input[AEC_FRAME_ADVANCE + 1 + 1], AEC_FRAME_ADVANCE * sizeof(int32_t)); + aec_state.main_state.y_hat[0].exp = input[AEC_FRAME_ADVANCE + 1]; aec_calc_coherence(&aec_state.main_state, 0); diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py index 510c7185..d8cf2c47 100644 --- a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py @@ -4,20 +4,6 @@ import py_vs_c_utils as pvc -# runtime: -# bypass = rand % 2 - -# y: -# hr = rand % 4 -# exp = -31,32 -# data rand >> hr -# 512 - -# y_hat: same as y - -# out: 2 x float_s32_t -# accuracy: 1<< 13 - def rand_int32_arr(rng, size=None, hr_max=1, min=np.iinfo(np.int32).min, max=np.iinfo(np.int32).max+1): hr = rng.integers(hr_max) data = rng.integers(min, max, size=size, dtype=np.int32) @@ -26,39 +12,39 @@ def rand_int32_arr(rng, size=None, hr_max=1, min=np.iinfo(np.int32).min, max=np. @pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 1, 9, 0]]) def test_calc_coherence(aec_obj, rng, dut_runner): - proc_fr_len = 512 + frame_advance = aec_obj.frame_advance - y_len = proc_fr_len + 2 # for hr and exp + y_len = frame_advance + 1 # for exp y_hat_len = y_len out_len = 2 * 2 # 2 x float_s32_t in_len = y_len + y_hat_len input_data = np.array([in_len, out_len], dtype=np.int32) - test_frames = 10 + test_frames = 1<<10 ref_coh = np.empty(0, dtype=np.float64) ref_coh_slow = np.empty(0, dtype=np.float64) for _ in range(test_frames): - y, y_hr = rand_int32_arr(rng, proc_fr_len, 4) + y, _ = rand_int32_arr(rng, frame_advance, 4) y_exp = rng.integers(-31, 32, size=1, dtype=np.int32) input_data = np.append(input_data, y_exp) - input_data = np.append(input_data, y_hr) input_data = np.append(input_data, y) - y_hat, y_hat_hr = rand_int32_arr(rng, proc_fr_len, 4) + y_hat, _ = rand_int32_arr(rng, frame_advance, 4) y_hat_exp = rng.integers(-31, 32, size=1, dtype=np.int32) input_data = np.append(input_data, y_hat_exp) - input_data = np.append(input_data, y_hat_hr) input_data = np.append(input_data, y_hat) y_fl = pvc.int32_to_double(y, y_exp) y_hat_fl = pvc.int32_to_double(y_hat, y_hat_exp) - aec_obj.y_data[0] = y_fl - aec_obj.y_hat[0] = y_hat + start_i = frame_advance + stop_i = frame_advance * 2 + aec_obj.y_data[0][start_i:stop_i] = y_fl + aec_obj.y_hat[0][start_i:stop_i] = y_hat_fl aec_obj.calc_coherence() ref_coh = np.append(ref_coh, aec_obj.coh) @@ -74,8 +60,5 @@ def test_calc_coherence(aec_obj, rng, dut_runner): dut_coh_slow = np.concatenate(op_split[1::2]) dut_coh_slow = pvc.float_s32_arr_to_double(dut_coh_slow) - print(ref_coh) - print(dut_coh) - print(ref_coh_slow) - print(dut_coh_slow) - + np.testing.assert_allclose(ref_coh, dut_coh, rtol=0, atol=2e-1) + np.testing.assert_allclose(ref_coh_slow, dut_coh_slow, rtol=0, atol=1e-3) From 000ab6ca7869c9ac042c50ddf6a42b141f917e86 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 3 Feb 2026 15:38:27 +0000 Subject: [PATCH 04/10] moved random gen into shared utils --- .../lib_aec/aec_unit_tests_new/test_calc_coherence.py | 10 ++-------- tests/lib_vnr/vnr_unit_tests/test_utils.py | 6 ------ .../vnr_unit_tests/test_vnr_extract_features.py | 4 ++-- .../vnr_unit_tests/test_vnr_form_input_frame.py | 4 ++-- tests/lib_vnr/vnr_unit_tests/test_vnr_full.py | 4 ++-- tests/lib_vnr/vnr_unit_tests/test_vnr_inference.py | 4 ++-- .../vnr_unit_tests/test_vnr_priv_feature_quantise.py | 4 ++-- tests/lib_vnr/vnr_unit_tests/test_vnr_priv_log2.py | 5 ++--- .../lib_vnr/vnr_unit_tests/test_vnr_priv_make_slice.py | 4 ++-- .../vnr_unit_tests/test_vnr_priv_mel_compute.py | 3 +-- .../vnr_unit_tests/test_vnr_priv_normalise_patch.py | 4 ++-- .../vnr_unit_tests/test_vnr_priv_output_dequantise.py | 3 +-- tests/shared/python/py_vs_c_utils.py | 6 ++++++ 13 files changed, 26 insertions(+), 35 deletions(-) diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py index d8cf2c47..b227270e 100644 --- a/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_coherence.py @@ -4,12 +4,6 @@ import py_vs_c_utils as pvc -def rand_int32_arr(rng, size=None, hr_max=1, min=np.iinfo(np.int32).min, max=np.iinfo(np.int32).max+1): - hr = rng.integers(hr_max) - data = rng.integers(min, max, size=size, dtype=np.int32) - data >>= hr - return data, hr - @pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 1, 9, 0]]) def test_calc_coherence(aec_obj, rng, dut_runner): frame_advance = aec_obj.frame_advance @@ -26,13 +20,13 @@ def test_calc_coherence(aec_obj, rng, dut_runner): ref_coh_slow = np.empty(0, dtype=np.float64) for _ in range(test_frames): - y, _ = rand_int32_arr(rng, frame_advance, 4) + y = pvc.rand_int32_arr(rng, frame_advance, 4) y_exp = rng.integers(-31, 32, size=1, dtype=np.int32) input_data = np.append(input_data, y_exp) input_data = np.append(input_data, y) - y_hat, _ = rand_int32_arr(rng, frame_advance, 4) + y_hat = pvc.rand_int32_arr(rng, frame_advance, 4) y_hat_exp = rng.integers(-31, 32, size=1, dtype=np.int32) input_data = np.append(input_data, y_hat_exp) diff --git a/tests/lib_vnr/vnr_unit_tests/test_utils.py b/tests/lib_vnr/vnr_unit_tests/test_utils.py index 2d6f4f74..ef0e303c 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_utils.py +++ b/tests/lib_vnr/vnr_unit_tests/test_utils.py @@ -7,12 +7,6 @@ BATCH_SIZE = vnr.PATCH_WIDTH * vnr.MEL_FILTERS -def rand_int32_arr(rng, size=None, hr_max=1, min=np.iinfo(np.int32).min, max=np.iinfo(np.int32).max+1): - hr = rng.integers(hr_max) - data = rng.integers(min, max, size=size, dtype=np.int32) - data >>= hr - return data - def stft(x_data, new_x_frame, x_data_len, new_x_frame_len, nfft): x_data = np.roll(x_data, -new_x_frame_len, axis=0) x_data[x_data_len - new_x_frame_len:] = new_x_frame diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_extract_features.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_extract_features.py index 2ab184f7..4f1d03af 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_extract_features.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_extract_features.py @@ -3,7 +3,7 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, stft, BATCH_SIZE +from test_utils import stft, BATCH_SIZE def test_vnr_extract_features(rng, quantise, vnr_obj, dut_runner): @@ -23,7 +23,7 @@ def test_vnr_extract_features(rng, quantise, vnr_obj, dut_runner): for _ in range(test_frames): enable_highpass = rng.integers(2) - data = rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) + data = pvc.rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) input_data = np.append(input_data, data) input_data = np.append(input_data, enable_highpass) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_form_input_frame.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_form_input_frame.py index 0e6d7dd7..893b4b70 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_form_input_frame.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_form_input_frame.py @@ -3,7 +3,7 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, stft +from test_utils import stft def test_vnr_form_input_frame(rng, dut_runner): input_words_per_frame = vnr.FRAME_ADVANCE #No. of int32 values sent to dut as input per frame @@ -19,7 +19,7 @@ def test_vnr_form_input_frame(rng, dut_runner): ref_output = np.empty(0, dtype=np.complex128) for _ in range(test_frames): # Generate input data - data = rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) + data = pvc.rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) input_data = np.append(input_data, data) # Ref form input frame implementation diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_full.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_full.py index 687b0d64..050c05ea 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_full.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_full.py @@ -3,7 +3,7 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, stft +from test_utils import stft def test_vnr_full(rng, vnr_obj, dut_runner): @@ -20,7 +20,7 @@ def test_vnr_full(rng, vnr_obj, dut_runner): for _ in range(test_frames): enable_highpass = rng.integers(2) # Generate input data - data = rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) + data = pvc.rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) input_data = np.append(input_data, data) input_data = np.append(input_data, enable_highpass) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_inference.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_inference.py index ccb7d3ad..8ab72763 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_inference.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_inference.py @@ -3,7 +3,7 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, BATCH_SIZE +from test_utils import BATCH_SIZE def test_vnr_inference(rng, vnr_obj, dut_runner): @@ -15,7 +15,7 @@ def test_vnr_inference(rng, vnr_obj, dut_runner): ref_output_double = np.empty(0, dtype=np.float64) for _ in range(test_frames): - data = rand_int32_arr(rng, BATCH_SIZE, max=0) + data = pvc.rand_int32_arr(rng, BATCH_SIZE, max=0) exp = rng.integers(-31, 0) # exp input_data = np.append(input_data, exp) input_data = np.append(input_data, data) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_feature_quantise.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_feature_quantise.py index 754d43cf..deded11a 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_feature_quantise.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_feature_quantise.py @@ -2,7 +2,7 @@ # This Software is subject to the terms of the XMOS Public Licence: Version 1. import numpy as np import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, BATCH_SIZE +from test_utils import BATCH_SIZE def test_vnr_priv_feature_quantise(rng, quantise, dut_runner): input_words_per_frame = BATCH_SIZE + 1 # 96 mantissas and 1 exponent @@ -13,7 +13,7 @@ def test_vnr_priv_feature_quantise(rng, quantise, dut_runner): ref_output = np.empty(0, dtype=np.int8) for _ in range(test_frames): # By setting high=1 we enure no value is greater than 0 since max normalised output is 0 - data = rand_int32_arr(rng, BATCH_SIZE, max=1) + data = pvc.rand_int32_arr(rng, BATCH_SIZE, max=1) exp = rng.integers(-31, 0) # exp input_data = np.append(input_data, exp) input_data = np.append(input_data, data) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_log2.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_log2.py index 07ecb316..d28d7fb6 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_log2.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_log2.py @@ -4,7 +4,6 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr def test_vnr_priv_log2(rng, dut_runner): @@ -21,8 +20,8 @@ def test_vnr_priv_log2(rng, dut_runner): for _ in range(test_frames): data = np.zeros(vnr.MEL_FILTERS*2, dtype=np.int32) - data[0::2] = rand_int32_arr(rng, vnr.MEL_FILTERS, 5, min=1) - data[1::2] = rand_int32_arr(rng, vnr.MEL_FILTERS, min=-32, max=16) # exp + data[0::2] = pvc.rand_int32_arr(rng, vnr.MEL_FILTERS, 5, min=1) + data[1::2] = pvc.rand_int32_arr(rng, vnr.MEL_FILTERS, min=-32, max=16) # exp data = np.array(data, dtype=np.int32) input_data = np.append(input_data, data) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_make_slice.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_make_slice.py index 883a5974..ba655ed1 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_make_slice.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_make_slice.py @@ -3,7 +3,7 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, stft +from test_utils import stft def test_vnr_priv_make_slice(rng, vnr_obj, dut_runner): @@ -20,7 +20,7 @@ def test_vnr_priv_make_slice(rng, vnr_obj, dut_runner): for _ in range(test_frames): enable_highpass = rng.integers(2) # Generate input data - data = rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) + data = pvc.rand_int32_arr(rng, vnr.FRAME_ADVANCE, 8) input_data = np.append(input_data, data) input_data = np.append(input_data, enable_highpass) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_mel_compute.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_mel_compute.py index 86da4b12..c7337f1e 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_mel_compute.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_mel_compute.py @@ -4,7 +4,6 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr def test_vnr_priv_mel_compute(rng, vnr_obj, dut_runner): @@ -22,7 +21,7 @@ def test_vnr_priv_mel_compute(rng, vnr_obj, dut_runner): ref_output_float = np.empty(0, dtype=np.float64) for _ in range(test_frames): exp = rng.integers(-32, -8) - data = rand_int32_arr(rng, fd_frame_len*2, 5) + data = pvc.rand_int32_arr(rng, fd_frame_len*2, 5) input_data = np.append(input_data, exp) input_data = np.append(input_data, data) diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_normalise_patch.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_normalise_patch.py index 4e48175a..412cd846 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_normalise_patch.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_normalise_patch.py @@ -3,7 +3,7 @@ import numpy as np import py_voice.modules.vnr as vnr import py_vs_c_utils as pvc -from test_utils import rand_int32_arr, BATCH_SIZE +from test_utils import BATCH_SIZE def test_vnr_priv_normalise_patch(rng, vnr_obj, dut_runner): @@ -17,7 +17,7 @@ def test_vnr_priv_normalise_patch(rng, vnr_obj, dut_runner): ref_output_float = np.empty(0, dtype=np.float64) for _ in range(test_frames): # Generate input data - data = rand_int32_arr(rng, vnr.MEL_FILTERS, 8) + data = pvc.rand_int32_arr(rng, vnr.MEL_FILTERS, 8) input_data = np.append(input_data, data) # Ref form input frame implementation diff --git a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_output_dequantise.py b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_output_dequantise.py index 1116612c..5a34809c 100644 --- a/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_output_dequantise.py +++ b/tests/lib_vnr/vnr_unit_tests/test_vnr_priv_output_dequantise.py @@ -1,7 +1,6 @@ # Copyright 2025-2026 XMOS LIMITED. # This Software is subject to the terms of the XMOS Public Licence: Version 1. import numpy as np -from test_utils import rand_int32_arr import py_vs_c_utils as pvc def test_vnr_priv_output_dequantise(rng, dequantise, dut_runner): @@ -14,7 +13,7 @@ def test_vnr_priv_output_dequantise(rng, dequantise, dut_runner): ref_output_double = np.empty(0, dtype=np.float64) for _ in range(test_frames): - data = rand_int32_arr(rng, 1, min=np.iinfo(np.int8).min, max=np.iinfo(np.int8).max + 1) + data = pvc.rand_int32_arr(rng, 1, min=np.iinfo(np.int8).min, max=np.iinfo(np.int8).max + 1) input_data = np.append(input_data, data) # Reference dequantise implementation diff --git a/tests/shared/python/py_vs_c_utils.py b/tests/shared/python/py_vs_c_utils.py index 715b5dea..59ebe8ab 100644 --- a/tests/shared/python/py_vs_c_utils.py +++ b/tests/shared/python/py_vs_c_utils.py @@ -6,6 +6,12 @@ from pathlib import Path import sys +def rand_int32_arr(rng, size=None, hr_max=1, min=np.iinfo(np.int32).min, max=np.iinfo(np.int32).max+1): + hr = rng.integers(hr_max) + data = rng.integers(min, max, size=size, dtype=np.int32) + data >>= hr + return data + # Turn a float32 from C into an np float scalar def float_s32_to_float(float_s32): return np.ldexp(float_s32.mant, float_s32.exp) From 314352f34facb8bb9eab3201ab991f2992ad17dd Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 3 Feb 2026 15:45:17 +0000 Subject: [PATCH 05/10] added test_aec_calc_corr_factor --- .../src/test_calc_corr_factor.c | 38 ++++++++++++++ .../test_calc_corr_factor.py | 50 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_calc_corr_factor.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_calc_corr_factor.py diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_corr_factor.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_corr_factor.c new file mode 100644 index 00000000..a5f84a18 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_corr_factor.c @@ -0,0 +1,38 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. +#include +#include +#include +#include +#include +#include "aec.h" +#include "aec_priv.h" + +#define TEST_FRAME_LEN (AEC_FRAME_ADVANCE - 32) + +static aec_state_t aec_state; +void test_init() +{ + #if BUILD_NATIVE + aec_task_distribution_t tdist = aec_tdist_chans2_threads1; + #else + aec_task_distribution_t tdist = aec_tdist_chans2_threads2; + #endif + + aec_init(&aec_state, 1, 1, 10, 0, &tdist); +} + +void test(int32_t *output, int32_t *input) +{ + // aec_calc_corr_factor uses y_hat[240:480-32] and prev_y[0:240-32] + aec_state.main_state.shared_state->prev_y[0].data = &input[1]; + aec_state.main_state.shared_state->prev_y[0].exp = input[0]; + + bfp_s32_init(&aec_state.main_state.y_hat[0], (int32_t*)&aec_state.main_state.Y_hat[0].data[0], 0, AEC_PROC_FRAME_LENGTH, 0); + memcpy(&aec_state.main_state.y_hat[0].data[AEC_FRAME_ADVANCE], &input[TEST_FRAME_LEN + 1 + 1], TEST_FRAME_LEN * sizeof(int32_t)); + aec_state.main_state.y_hat[0].exp = input[AEC_PROC_FRAME_LENGTH + 1]; + + float_s32_t corr = aec_calc_corr_factor(&aec_state.main_state, 0); + + memcpy(output, &corr, sizeof(float_s32_t)); +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_corr_factor.py b/tests/lib_aec/aec_unit_tests_new/test_calc_corr_factor.py new file mode 100644 index 00000000..6f66b920 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_corr_factor.py @@ -0,0 +1,50 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc + + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 1, 10, 0]]) +def test_calc_corr_factor(aec_obj, rng, dut_runner): + frame_advance = aec_obj.frame_advance + test_fr_len = frame_advance - aec_obj.overlap_width + + y_len = test_fr_len + 1 # for exp + y_hat_len = y_len + + out_len = 2 # float_s32_t + in_len = y_len + y_hat_len + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 1<<10 + ref_corr = np.empty(0, dtype=np.float64) + + for _ in range(test_frames): + y= pvc.rand_int32_arr(rng, test_fr_len, 4) + y_exp = rng.integers(-31, 32, size=1, dtype=np.int32) + + input_data = np.append(input_data, y_exp) + input_data = np.append(input_data, y) + + y_hat= pvc.rand_int32_arr(rng, test_fr_len, 4) + y_hat_exp = rng.integers(-31, 32, size=1, dtype=np.int32) + + input_data = np.append(input_data, y_hat_exp) + input_data = np.append(input_data, y_hat) + + y_fl = pvc.int32_to_double(y, y_exp) + y_hat_fl = pvc.int32_to_double(y_hat, y_hat_exp) + + start_i = frame_advance + stop_i = start_i + test_fr_len + aec_obj.y_data[0][start_i:stop_i] = y_fl + aec_obj.y_hat[0][start_i:stop_i] = y_hat_fl + ref_corr_one = aec_obj.calc_corr_factor() + + ref_corr = np.append(ref_corr, ref_corr_one) + + op = dut_runner(input_data) + + dut_corr = pvc.float_s32_arr_to_double(op) + + np.testing.assert_allclose(ref_corr, dut_corr, rtol=0, atol=1e-9) From 2dbeb94fcf6e66f76083f8f39f928429d5ee1e7c Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Thu, 5 Feb 2026 11:47:49 +0000 Subject: [PATCH 06/10] adding calc_Error_and_Y_hat test --- tests/lib_aec/aec_unit_tests_new/conftest.py | 16 +++- .../src/test_calc_Error_and_Y_hat.c | 78 ++++++++++++++++ .../test_calc_Error_and_Y_hat.py | 90 +++++++++++++++++++ tests/lib_vnr/vnr_unit_tests/src/main.c | 4 +- tests/shared/python/py_vs_c_utils.py | 2 + 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_calc_Error_and_Y_hat.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py diff --git a/tests/lib_aec/aec_unit_tests_new/conftest.py b/tests/lib_aec/aec_unit_tests_new/conftest.py index 193614d2..363d187e 100644 --- a/tests/lib_aec/aec_unit_tests_new/conftest.py +++ b/tests/lib_aec/aec_unit_tests_new/conftest.py @@ -4,6 +4,7 @@ from pathlib import Path import py_voice.modules.aec as aec import py_voice.config.config as pv_config +import py_voice.core.fdaf_controller as fdc import py_voice import numpy as np from run_dut import run_dut @@ -13,8 +14,7 @@ default_conf = pv_config.get_config_dict(default_conf_path) bin_dir_path = Path(__file__).parent / "bin" -@pytest.fixture -def aec_obj(y_ch, x_ch, main_ph, shadow_ph): +def set_aec_conf(y_ch, x_ch, main_ph, shadow_ph): test_conf = default_conf test_conf["general"]["modules"] = ["aec"] test_conf["general"]["input_channel_count"] = y_ch + x_ch @@ -26,8 +26,20 @@ def aec_obj(y_ch, x_ch, main_ph, shadow_ph): test_conf["aec"]["phases"] = main_ph test_conf["aec"]["phases_shadow"] = shadow_ph + return test_conf + +@pytest.fixture +def aec_obj(y_ch, x_ch, main_ph, shadow_ph): + test_conf = set_aec_conf(y_ch, x_ch, main_ph, shadow_ph) + return aec.aec(test_conf) +@pytest.fixture +def fdaf_obj(y_ch, x_ch, main_ph, shadow_ph): + test_conf = set_aec_conf(y_ch, x_ch, main_ph, shadow_ph) + + return fdc.fdaf_controller(test_conf, "aec") + @pytest.fixture def dut_runner(request, target): exe_path = bin_dir_path / request.node.originalname / f"aec_unit_tests_new_{request.node.originalname}" diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_Error_and_Y_hat.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_Error_and_Y_hat.c new file mode 100644 index 00000000..1bada14b --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_Error_and_Y_hat.c @@ -0,0 +1,78 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. +#include +#include +#include +#include +#include +#include "aec.h" +#include "aec_priv.h" + +#define Y_CH 1 +#define X_CH 2 +#define MAIN_PH 5 +#define SHADOW_PH 3 + +#define NUM_BINS ((AEC_PROC_FRAME_LENGTH / 2) + 1) +#define BFP_LEN (NUM_BINS * 2) +#define BFP_BIN_LEN (BFP_LEN + 1) // for exponent +#define Y_LEN (Y_CH * BFP_BIN_LEN) +#define X_LEN (X_CH * MAIN_PH * BFP_BIN_LEN) +#define H_HAT_LEN (Y_CH * X_CH * MAIN_PH * BFP_BIN_LEN) + +static aec_state_t aec_state; +void test_init() +{ + #if BUILD_NATIVE + aec_task_distribution_t tdist = aec_tdist_chans2_threads1; + #else + aec_task_distribution_t tdist = aec_tdist_chans2_threads2; + #endif + + aec_init(&aec_state, Y_CH, X_CH, MAIN_PH, SHADOW_PH, &tdist); +} + +void test(int32_t *output, int32_t *input) +{ + aec_filter_state_t *state_ptr = &aec_state.main_state; + for(unsigned ch = 0; ch < Y_CH; ch++){ + unsigned offset = ch * BFP_BIN_LEN; + bfp_complex_s32_init(&state_ptr->shared_state->Y[ch], (complex_s32_t *)&input[offset + 1], input[offset], NUM_BINS, 1); + } + + for(unsigned ch = 0; ch < X_CH; ch++) { + for(unsigned ph = 0; ph < MAIN_PH; ph++) { + unsigned offset = (ch * MAIN_PH + ph) * BFP_BIN_LEN + Y_LEN; + bfp_complex_s32_init(&state_ptr->shared_state->X_fifo[ch][ph], (complex_s32_t *)&input[offset + 1], input[offset], NUM_BINS, 1); + } + } + //aec init only initialises the 2d Xfifo. Since we're using the 1d fifo for error computation, call aec_update_X_fifo_1d() + //to update the 1d Fifo + aec_update_X_fifo_1d(state_ptr); + + for(unsigned ch = 0; ch < Y_CH; ch++) { + for(unsigned ph = 0; ph < (X_CH * MAIN_PH); ph++) { + unsigned offset = (ch * (X_CH * MAIN_PH) + ph) * BFP_BIN_LEN + Y_LEN + X_LEN; + bfp_complex_s32_init(&state_ptr->H_hat[ch][ph], (complex_s32_t *)&input[offset + 1], input[offset], NUM_BINS, 1); + } + } + + for(unsigned ch = 0; ch < Y_CH; ch++) { + // Y_hat is accumulated via bfp_complex_s32_macc() inside aec_l2_calc_Error_and_Y_hat(). + // The production pipeline clears Y_hat each frame in aec_process_frame(); do the same here + // to keep frames independent and prevent drift. + state_ptr->Y_hat[ch].exp = AEC_ZEROVAL_EXP; + state_ptr->Y_hat[ch].hr = AEC_ZEROVAL_HR; + memset(&state_ptr->Y_hat[ch].data[0], 0, NUM_BINS * sizeof(complex_s32_t)); + + aec_calc_Error_and_Y_hat(state_ptr, ch); + + unsigned offset = ch * BFP_BIN_LEN * 2; + unsigned offset2 = offset + BFP_BIN_LEN; + + memcpy(&output[offset], &state_ptr->Y_hat[ch].exp, sizeof(int32_t)); + memcpy(&output[offset + 1], state_ptr->Y_hat[ch].data, BFP_LEN * sizeof(int32_t)); + memcpy(&output[offset2], &state_ptr->Error[ch].exp, sizeof(int32_t)); + memcpy(&output[offset2 + 1], state_ptr->Error[ch].data, BFP_LEN * sizeof(int32_t)); + } +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py b/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py new file mode 100644 index 00000000..52a8aa3a --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py @@ -0,0 +1,90 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc + +def float64_to_comp128_reshape(data, shape): + data = data.view(np.complex128) + data = data.reshape(shape) + return data + +def gen_bfp(rng, len, hr_max, exps=(-31, 31)): + data = pvc.rand_int32_arr(rng, len, hr_max) + exp = rng.integers(exps[0], exps[1] + 1, size=1, dtype=np.int32) + data_fl64 = pvc.int32_to_double(data, exp) + data_int = np.concatenate([exp, data]) + return data_int, data_fl64 + +def gen_bfps(rng, hr_max, shape, exps=(-31, 31)): + bfp_len = shape[-1] + bfp_num = 1 + data_int = np.empty(0, dtype=np.int32) + data_fl64 = np.empty(0, dtype=np.float64) + for dim in range(len(shape) - 1): + bfp_num *= shape[dim] + + for _ in range(bfp_num): + bfp_int, arr_fl64 = gen_bfp(rng, bfp_len, hr_max, exps) + data_int = np.append(data_int, bfp_int) + data_fl64 = np.append(data_fl64, arr_fl64) + + return data_int, data_fl64 + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 2, 5, 3]]) +def test_calc_Error_and_Y_hat(fdaf_obj, y_ch, x_ch, main_ph, rng, dut_runner): + f_bin_count = fdaf_obj.f_bin_count + + # all complex and need an exponent (per BFP) + bfp_size = f_bin_count * 2 + 1 # exponent + complex data (real + imag for each bin) + Y_len = y_ch * bfp_size + H_hat_len = y_ch * x_ch * main_ph * bfp_size + X_len = x_ch * main_ph * bfp_size + Error_len = Y_len + Y_hat_len = Y_len + + out_len = Error_len + Y_hat_len + in_len = Y_len + H_hat_len + X_len + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 1<<10 + ref_Error = np.empty(0, dtype=np.float64) + ref_Y_hat = np.empty(0, dtype=np.float64) + + for _ in range(test_frames): + # Keep exponent ranges small so the reference doesn't hit huge |X*H| products + # that make Error ~= -Y_hat due to |Y_hat| >> |Y|. + Y, Y_fl = gen_bfps(rng, 3, (y_ch, f_bin_count * 2), (-30, -24)) + input_data = np.append(input_data, Y) + Y_fl = float64_to_comp128_reshape(Y_fl, (y_ch, f_bin_count)) + + X, X_fl = gen_bfps(rng, 3, (x_ch, main_ph, f_bin_count * 2), (-30, -24)) + input_data = np.append(input_data, X) + X_fl = float64_to_comp128_reshape(X_fl, (x_ch, main_ph, f_bin_count)) + + # H_hat has 3 dimentions in C and 4 in python + H_hat, H_hat_fl = gen_bfps(rng, 3, (y_ch, x_ch * main_ph, f_bin_count * 2), (-30, -24)) + input_data = np.append(input_data, H_hat) + H_hat_fl = float64_to_comp128_reshape(H_hat_fl, (y_ch, x_ch, main_ph, f_bin_count)) + + # Set state on the filter where calc_Error_and_Y_hat will access it + fdaf_obj.main_filter.X_data[:] = X_fl + fdaf_obj.main_filter.H[:] = H_hat_fl + fdaf_obj.main_filter.calc_Error_and_Y_hat(Y_fl) + + ref_Y_hat = np.append(ref_Y_hat, fdaf_obj.main_filter.Y_hat.view(np.float64)) + ref_Error = np.append(ref_Error, fdaf_obj.main_filter.Error.view(np.float64)) + + op = dut_runner(input_data) + + # Both Y_hat and Error have [y_ch][f_bin_count] dimentions and are complex + sections = np.cumsum(np.tile([bfp_size, bfp_size], test_frames * y_ch))[:-1].astype(np.int32) + op_split = np.split(op, sections) + + dut_Y_hat = np.concatenate(op_split[0::2]) + dut_Y_hat = pvc.bfp_s32_arr_to_double(dut_Y_hat, f_bin_count * 2, test_frames) + dut_Error = np.concatenate(op_split[1::2]) + dut_Error = pvc.bfp_s32_arr_to_double(dut_Error, f_bin_count * 2, test_frames) + + np.testing.assert_allclose(ref_Y_hat, dut_Y_hat, rtol=0, atol=2e-4) + np.testing.assert_allclose(ref_Error, dut_Error, rtol=0, atol=2e-4) + diff --git a/tests/lib_vnr/vnr_unit_tests/src/main.c b/tests/lib_vnr/vnr_unit_tests/src/main.c index 8ac07248..ffbbde1e 100644 --- a/tests/lib_vnr/vnr_unit_tests/src/main.c +++ b/tests/lib_vnr/vnr_unit_tests/src/main.c @@ -34,8 +34,8 @@ void test_vnr_unit(const char *input_file_name, const char *output_file_name) int num_frames = (filesize_words - 2)/input_framesize; printf("num_frames = %d\n",num_frames); - int32_t biggest_possible_input_frame[2048]; - int32_t biggest_possible_output_frame[512]; + int32_t biggest_possible_input_frame[12000]; + int32_t biggest_possible_output_frame[2048]; test_init(); for(int fr=0; fr Date: Thu, 5 Feb 2026 14:17:26 +0000 Subject: [PATCH 07/10] added test_calc_freq_domain_energy --- .../src/test_calc_freq_domain_energy.c | 34 +++++++++++++++++ .../test_calc_freq_domain_energy.py | 38 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_calc_freq_domain_energy.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_calc_freq_domain_energy.py diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_freq_domain_energy.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_freq_domain_energy.c new file mode 100644 index 00000000..c6a3ead5 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_freq_domain_energy.c @@ -0,0 +1,34 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. +#include +#include +#include +#include +#include +#include "aec.h" +#include "aec_priv.h" + +#define TEST_FRAME_LEN (AEC_PROC_FRAME_LENGTH / 2 + 1) + +static aec_state_t aec_state; +void test_init() +{ + #if BUILD_NATIVE + aec_task_distribution_t tdist = aec_tdist_chans2_threads1; + #else + aec_task_distribution_t tdist = aec_tdist_chans2_threads2; + #endif + + aec_init(&aec_state, 1, 1, 10, 0, &tdist); +} + +void test(int32_t *output, int32_t *input) +{ + bfp_complex_s32_t data; + float_s32_t energy; + bfp_complex_s32_init(&data, (int32_t *)&input[1], input[0], TEST_FRAME_LEN, 1); + + aec_calc_freq_domain_energy(&energy, &data); + + memcpy(output, &energy, sizeof(float_s32_t)); +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_freq_domain_energy.py b/tests/lib_aec/aec_unit_tests_new/test_calc_freq_domain_energy.py new file mode 100644 index 00000000..dcb668a7 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_freq_domain_energy.py @@ -0,0 +1,38 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 1, 10, 0]]) +def test_calc_freq_domain_energy(aec_obj, rng, dut_runner): + fd_frame_len = aec_obj.f_bin_count + + data_len = fd_frame_len * 2 + 1 # for exp + + out_len = 2 # float_s32_t + in_len = data_len + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 1<<10 + ref_energy = np.empty(0, dtype=np.float64) + + for _ in range(test_frames): + data = pvc.rand_int32_arr(rng, fd_frame_len * 2, 4) + data_exp = rng.integers(-31, -24, size=1, dtype=np.int32) + + input_data = np.append(input_data, data_exp) + input_data = np.append(input_data, data) + + data_fl = pvc.int32_to_double(data, data_exp) + data_fl = data_fl.view(np.complex128) + data_fl = data_fl.reshape((1, fd_frame_len)) + + ref_energy_one = aec_obj.calc_ov_energy(data_fl) + + ref_energy = np.append(ref_energy, ref_energy_one) + + op = dut_runner(input_data) + + dut_energy = pvc.float_s32_arr_to_double(op) + + np.testing.assert_allclose(ref_energy, dut_energy, rtol=0, atol=6e-4) From 0865c81e9effa479f5fb75c575d3ca4addeda3d2 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 6 Feb 2026 14:05:53 +0000 Subject: [PATCH 08/10] added test_calc_inv_X_energy --- tests/lib_aec/aec_unit_tests_new/conftest.py | 23 ++++++ .../src/test_calc_inv_X_energy.c | 54 +++++++++++++ .../test_calc_Error_and_Y_hat.py | 23 +----- .../test_calc_inv_X_energy.py | 81 +++++++++++++++++++ 4 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_calc_inv_X_energy.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_calc_inv_X_energy.py diff --git a/tests/lib_aec/aec_unit_tests_new/conftest.py b/tests/lib_aec/aec_unit_tests_new/conftest.py index 363d187e..d23d33eb 100644 --- a/tests/lib_aec/aec_unit_tests_new/conftest.py +++ b/tests/lib_aec/aec_unit_tests_new/conftest.py @@ -7,6 +7,7 @@ import py_voice.core.fdaf_controller as fdc import py_voice import numpy as np +import py_vs_c_utils as pvc from run_dut import run_dut PY_VOICE_ROOT = Path(py_voice.__file__).resolve().parent @@ -14,6 +15,28 @@ default_conf = pv_config.get_config_dict(default_conf_path) bin_dir_path = Path(__file__).parent / "bin" +def gen_bfp(rng, len, hr_max, exps=(-31, 31)): + data = pvc.rand_int32_arr(rng, len, hr_max) + exp = rng.integers(exps[0], exps[1] + 1, size=1, dtype=np.int32) + data_fl64 = pvc.int32_to_double(data, exp) + data_int = np.concatenate([exp, data]) + return data_int, data_fl64 + +def gen_bfps(rng, hr_max, shape, exps=(-31, 31)): + bfp_len = shape[-1] + bfp_num = 1 + data_int = np.empty(0, dtype=np.int32) + data_fl64 = np.empty(0, dtype=np.float64) + for dim in range(len(shape) - 1): + bfp_num *= shape[dim] + + for _ in range(bfp_num): + bfp_int, arr_fl64 = gen_bfp(rng, bfp_len, hr_max, exps) + data_int = np.append(data_int, bfp_int) + data_fl64 = np.append(data_fl64, arr_fl64) + + return data_int, data_fl64 + def set_aec_conf(y_ch, x_ch, main_ph, shadow_ph): test_conf = default_conf test_conf["general"]["modules"] = ["aec"] diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_inv_X_energy.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_inv_X_energy.c new file mode 100644 index 00000000..a3f73fb8 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_inv_X_energy.c @@ -0,0 +1,54 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. +#include +#include +#include +#include +#include +#include "aec.h" +#include "aec_priv.h" + +#define Y_CH 1 +#define X_CH 2 +#define MAIN_PH 6 +#define SHADOW_PH 2 + +#define NUM_BINS ((AEC_PROC_FRAME_LENGTH / 2) + 1) +#define BFP_BIN_LEN (NUM_BINS + 1) // for exponent + +static aec_state_t aec_state; +void test_init() +{ + #if BUILD_NATIVE + aec_task_distribution_t tdist = aec_tdist_chans2_threads1; + #else + aec_task_distribution_t tdist = aec_tdist_chans2_threads2; + #endif + + aec_init(&aec_state, Y_CH, X_CH, MAIN_PH, SHADOW_PH, &tdist); +} + +void test(int32_t *output, int32_t *input) +{ + int32_t is_shadow = input[0]; + float_s32_t delta = {input[1], input[2]}; + + aec_filter_state_t *state_ptr = (is_shadow) ? &aec_state.shadow_state : &aec_state.main_state; + + state_ptr->delta = delta; + for(unsigned ch = 0; ch < X_CH; ch++) { + unsigned offset = 3 + ch * BFP_BIN_LEN; + // unsigned offset2 = 3 + int32_t scratch[NUM_BINS] = {0}; + bfp_s32_init(&state_ptr->shared_state->sigma_XX[ch], &input[offset + 1], input[offset], NUM_BINS, 1); + offset += BFP_BIN_LEN * 2; + bfp_s32_init(&state_ptr->X_energy[ch], &input[offset + 1], input[offset], NUM_BINS, 1); + bfp_s32_init(&state_ptr->inv_X_energy[ch], scratch, 0, NUM_BINS, 0); + + aec_calc_normalisation_spectrum(state_ptr, ch, is_shadow); + + unsigned out_offset = ch * BFP_BIN_LEN; + memcpy(&output[out_offset], &state_ptr->inv_X_energy[ch].exp, sizeof(int32_t)); + memcpy(&output[out_offset + 1], state_ptr->inv_X_energy[ch].data, NUM_BINS * sizeof(int32_t)); + } +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py b/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py index 52a8aa3a..bc12bc1c 100644 --- a/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_Error_and_Y_hat.py @@ -2,34 +2,13 @@ import pytest import numpy as np import py_vs_c_utils as pvc +from conftest import gen_bfps def float64_to_comp128_reshape(data, shape): data = data.view(np.complex128) data = data.reshape(shape) return data -def gen_bfp(rng, len, hr_max, exps=(-31, 31)): - data = pvc.rand_int32_arr(rng, len, hr_max) - exp = rng.integers(exps[0], exps[1] + 1, size=1, dtype=np.int32) - data_fl64 = pvc.int32_to_double(data, exp) - data_int = np.concatenate([exp, data]) - return data_int, data_fl64 - -def gen_bfps(rng, hr_max, shape, exps=(-31, 31)): - bfp_len = shape[-1] - bfp_num = 1 - data_int = np.empty(0, dtype=np.int32) - data_fl64 = np.empty(0, dtype=np.float64) - for dim in range(len(shape) - 1): - bfp_num *= shape[dim] - - for _ in range(bfp_num): - bfp_int, arr_fl64 = gen_bfp(rng, bfp_len, hr_max, exps) - data_int = np.append(data_int, bfp_int) - data_fl64 = np.append(data_fl64, arr_fl64) - - return data_int, data_fl64 - @pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 2, 5, 3]]) def test_calc_Error_and_Y_hat(fdaf_obj, y_ch, x_ch, main_ph, rng, dut_runner): f_bin_count = fdaf_obj.f_bin_count diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_inv_X_energy.py b/tests/lib_aec/aec_unit_tests_new/test_calc_inv_X_energy.py new file mode 100644 index 00000000..d7cd4584 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_inv_X_energy.py @@ -0,0 +1,81 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc +from conftest import gen_bfps + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 2, 6, 2]]) +def test_calc_inv_X_energy(aec_obj, x_ch, rng, dut_runner): + f_bin_count = aec_obj.f_bin_count + + sigma_xx_len = x_ch * (f_bin_count + 1) # for exp + X_energy_len = sigma_xx_len + delta_len = 2 # float_s32_t + + inv_X_energy_len = sigma_xx_len + + in_len = sigma_xx_len + X_energy_len + delta_len + 1 # for is_shadow + out_len = inv_X_energy_len + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 1<<10 + ref_inv_X_energy = np.empty(0, dtype=np.float64) + + for _ in range(test_frames): + is_shadow = rng.integers(2, size=1, dtype=np.int32) + input_data = np.append(input_data, is_shadow) + + delta_mant = pvc.rand_int32_arr(rng, size=1) + delta_exp = rng.integers(-95, -31, size=1, dtype=np.int32) + input_data = np.append(input_data, [delta_mant, delta_exp]) + delta_fl = np.ldexp(delta_mant, delta_exp) + + sigma_xx, sigma_xx_fl = gen_bfps(rng, 4, (x_ch, f_bin_count), (-31, 31)) + input_data = np.append(input_data, sigma_xx) + sigma_xx_fl = sigma_xx_fl.reshape((x_ch, f_bin_count)) + + X_energy, X_energy_fl = gen_bfps(rng, 4, (x_ch, f_bin_count), (-31, 31)) + input_data = np.append(input_data, X_energy) + X_energy_fl = X_energy_fl.reshape((x_ch, f_bin_count)) + + aec_obj.sigma_xx = sigma_xx_fl + + if not is_shadow: + aec_obj.main_filter.X_energy = X_energy_fl + aec_obj.main_filter.delta = delta_fl + inv_X_energy_one = aec_obj.main_filter.calc_inv_x_energy(aec_obj) + else: + aec_obj.shadow_filter.X_energy = X_energy_fl + aec_obj.shadow_filter.delta = delta_fl + inv_X_energy_one = aec_obj.shadow_filter.calc_inv_x_energy(aec_obj) + + ref_inv_X_energy = np.append(ref_inv_X_energy, inv_X_energy_one.real) + + # Extra test, for the case where delta is zero and X_energy is zero + # currenty handled differently in python and C, hence uncommented.. + + # test_frames += 1 + # input_data = np.append(input_data, 1) # shadow only + # input_data = np.append(input_data, [0, -1024]) # zero delta + # input_data = np.append(input_data, sigma_xx) # same sigma + # # getting 0/int32max * 2 ** exp array from X_energy + # (X_energy_exp0, X_energy_exp1) = (X_energy[0], X_energy[1 + f_bin_count]) + # X_energy = np.signbit(X_energy).astype(np.int32) * np.iinfo(np.int32).max + # X_energy[0] = X_energy_exp0 + # X_energy[1 + f_bin_count] = X_energy_exp1 + # input_data = np.append(input_data, X_energy) + + # X_energy_fl[0][:] = pvc.int32_to_double(X_energy[1:f_bin_count + 1], X_energy_exp0) + # X_energy_fl[1][:] = pvc.int32_to_double(X_energy[f_bin_count + 2:], X_energy_exp1) + # aec_obj.shadow_filter.X_energy = X_energy_fl + # # not setting the delta this time letting the API handle it + # aec_obj.shadow_filter.calc_delta() + # inv_X_energy_one = aec_obj.shadow_filter.calc_inv_x_energy(aec_obj) + + # ref_inv_X_energy = np.append(ref_inv_X_energy, inv_X_energy_one.real) + + op = dut_runner(input_data) + + dut_inv_X_energy = pvc.bfp_s32_arr_to_double(op, f_bin_count, test_frames * x_ch) + + np.testing.assert_allclose(ref_inv_X_energy, dut_inv_X_energy, rtol=1e-3, atol=1e-6) From 751b4729d4a1da41ca787fd6e1258a068bfb43af Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 9 Feb 2026 11:30:41 +0000 Subject: [PATCH 09/10] added test_calc_max_ref_energy --- .../src/test_calc_max_ref_energy.c | 20 +++++++++++ .../test_calc_max_ref_energy.py | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_calc_max_ref_energy.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_calc_max_ref_energy.py diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_calc_max_ref_energy.c b/tests/lib_aec/aec_unit_tests_new/src/test_calc_max_ref_energy.c new file mode 100644 index 00000000..2efa1686 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_calc_max_ref_energy.c @@ -0,0 +1,20 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. + +#include +#include + +#include "aec.h" + +#define NUM_CHANNELS (4) + +void test_init() {} + +void test(int32_t *output, int32_t *input) +{ + // const int32_t (*input_data)[AEC_FRAME_ADVANCE] = (const int32_t (*)[AEC_FRAME_ADVANCE]) &input[0]; + + // float_s32_t max_energy = aec_calc_max_input_energy(input_data, NUM_CHANNELS); + float_s32_t max_energy = aec_calc_max_input_energy(input, NUM_CHANNELS); + memcpy(output, &max_energy, sizeof(float_s32_t)); +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_calc_max_ref_energy.py b/tests/lib_aec/aec_unit_tests_new/test_calc_max_ref_energy.py new file mode 100644 index 00000000..4ec44e66 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_calc_max_ref_energy.py @@ -0,0 +1,36 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc + + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 4, 10, 0]]) +def test_calc_max_ref_energy(aec_obj, x_ch, rng, dut_runner): + frame_advance = aec_obj.frame_advance + + in_len = x_ch * frame_advance + out_len = 2 # float_s32_t + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 1 << 10 + ref_energy = np.empty(0, dtype=np.float64) + + for _ in range(test_frames): + frame_int = np.empty((x_ch, frame_advance), dtype=np.int32) + for ch in range(x_ch): + frame_int[ch] = pvc.rand_int32_arr(rng, frame_advance, hr_max=12) + + input_data = np.append(input_data, frame_int.ravel()) + + far_frame = pvc.int32_to_double(frame_int, np.int32(-31)) + + # py_voice returns mean squared power; C returns sum of squares (energy). + ref_power = aec_obj.get_max_ref_power(far_frame) + ref_energy_one = ref_power * frame_advance + ref_energy = np.append(ref_energy, ref_energy_one) + + op = dut_runner(input_data) + + dut_energy = pvc.float_s32_arr_to_double(op) + + np.testing.assert_allclose(ref_energy, dut_energy, rtol=0, atol=1e-7) From 9acdf97ecd1a1a9fcb393ac21343f99e36791d0a Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 9 Feb 2026 11:31:07 +0000 Subject: [PATCH 10/10] added test_create_output --- .../src/test_create_output.c | 61 +++++++++++++++++++ .../aec_unit_tests_new/test_create_output.py | 58 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 tests/lib_aec/aec_unit_tests_new/src/test_create_output.c create mode 100644 tests/lib_aec/aec_unit_tests_new/test_create_output.py diff --git a/tests/lib_aec/aec_unit_tests_new/src/test_create_output.c b/tests/lib_aec/aec_unit_tests_new/src/test_create_output.c new file mode 100644 index 00000000..03ae0ba7 --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/src/test_create_output.c @@ -0,0 +1,61 @@ +// Copyright 2022-2026 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. + +#include +#include + +#include "aec.h" +#include "aec_priv.h" + +#define Y_CH (1) +#define X_CH (1) +#define MAIN_PH (9) +#define SHADOW_PH (9) + +static aec_state_t aec_state; + +void test_init() +{ +#if BUILD_NATIVE + aec_task_distribution_t tdist = aec_tdist_chans2_threads1; +#else + aec_task_distribution_t tdist = aec_tdist_chans2_threads2; +#endif + + aec_init(&aec_state, Y_CH, X_CH, MAIN_PH, SHADOW_PH, &tdist); + + // aec_state.main_state.error is normally initialised during the Error->error IFFT call. + // Initialise here for standalone testing, reusing Error complex buffer memory. + bfp_s32_init(&aec_state.main_state.error[0], (int32_t*)&aec_state.main_state.Error[0].data[0], 0, AEC_PROC_FRAME_LENGTH, 0); +} + +void test(int32_t *output, int32_t *input) +{ + // Input layout per frame: + // [error_exp, error_data[0:PROC_FRAME_LEN-1]] + const int32_t error_exp = input[0]; + const int32_t *error_data = &input[1]; + + bfp_s32_t *err = &aec_state.main_state.error[0]; + err->exp = error_exp; + memcpy(err->data, error_data, AEC_PROC_FRAME_LENGTH * sizeof(int32_t)); + err->hr = bfp_s32_headroom(err); + + int32_t out_frame[1][AEC_FRAME_ADVANCE]; + aec_calc_output(&aec_state.main_state, out_frame, 0); + + // Output layout per frame: + // [output_q31[0:AEC_FRAME_ADVANCE-1], overlap_exp, overlap_data[0:31], error_exp, error_data[0:PROC_FRAME_LEN-1]] + unsigned o = 0; + + memcpy(&output[o], &out_frame[0][0], AEC_FRAME_ADVANCE * sizeof(int32_t)); + o += AEC_FRAME_ADVANCE; + + bfp_s32_t *ov = &aec_state.main_state.overlap[0]; + output[o++] = ov->exp; + memcpy(&output[o], ov->data, ov->length * sizeof(int32_t)); + o += ov->length; + + output[o++] = err->exp; + memcpy(&output[o], err->data, err->length * sizeof(int32_t)); +} diff --git a/tests/lib_aec/aec_unit_tests_new/test_create_output.py b/tests/lib_aec/aec_unit_tests_new/test_create_output.py new file mode 100644 index 00000000..d96dd32f --- /dev/null +++ b/tests/lib_aec/aec_unit_tests_new/test_create_output.py @@ -0,0 +1,58 @@ + +import pytest +import numpy as np +import py_vs_c_utils as pvc + + +@pytest.mark.parametrize("y_ch, x_ch, main_ph, shadow_ph", [[1, 1, 9, 9]]) +def test_create_output(aec_obj, rng, dut_runner): + frame_advance = aec_obj.frame_advance + proc_frame_length = aec_obj.proc_frame_length + overlap_width = aec_obj.overlap_width + + in_len = 1 + proc_frame_length # for exp + out_len = frame_advance + (1 + overlap_width) + (1 + proc_frame_length) + input_data = np.array([in_len, out_len], dtype=np.int32) + + test_frames = 1 << 10 + ref_out = np.empty(0, dtype=np.float64) + ref_ov = np.empty(0, dtype=np.float64) + ref_err = np.empty(0, dtype=np.float64) + + # In this test we have to set the same exponent for all the tests as + # doing otherwise will make C to saturate (cause it has to output in q0.31) + # whereas python does not do so. + error_exp = rng.integers(-31, -27, size=1, dtype=np.int32) + + for _ in range(test_frames): + err_int = pvc.rand_int32_arr(rng, proc_frame_length, hr_max=4) + + input_data = np.append(input_data, error_exp) + input_data = np.append(input_data, err_int) + + err_fl = pvc.int32_to_double(err_int, error_exp).reshape(1, proc_frame_length) + + # Apply the same time-domain modifications as C (zero + window) before overlap-add. + # window_error mutates err_fl in-place. + aec_obj.window_error(err_fl) + # aec.created output would overwrite the error memory so coppying the array before calling the API + err_win_fl = err_fl.copy() + out_fl = aec_obj.create_output(err_fl) + + ref_out = np.append(ref_out, out_fl.ravel()) + ref_ov = np.append(ref_ov, aec_obj.overlap.ravel()) + ref_err = np.append(ref_err, err_win_fl.ravel()) + + op = dut_runner(input_data) + op = op.reshape(test_frames, out_len) + + dut_out_int = op[:, :frame_advance].ravel().astype(np.int32) + dut_out = pvc.int32_to_double(dut_out_int, np.int32(-31)) + + dut_ov = pvc.bfp_s32_arr_to_double(op[:, frame_advance: frame_advance + 1 + overlap_width].ravel(), overlap_width, test_frames) + + dut_err = pvc.bfp_s32_arr_to_double(op[:, frame_advance + 1 + overlap_width:].ravel(), proc_frame_length, test_frames) + + np.testing.assert_allclose(ref_out, dut_out, rtol=0, atol=3e-7) + np.testing.assert_allclose(ref_ov, dut_ov, rtol=0, atol=3e-7) + np.testing.assert_allclose(ref_err, dut_err, rtol=0, atol=3e-7)