This commit is contained in:
Steven Dan
2025-12-11 09:43:42 +08:00
commit d8b2974133
1822 changed files with 280037 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
project(fir_direct_test)
set(APP_HW_TARGET XCORE-AI-EXPLORER)
set(APP_COMPILER_FLAGS
-O3
-g
-report
-Wall
-Werror
-fxscope)
set(APP_DEPENDENT_MODULES lib_audio_dsp)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()

View File

View File

@@ -0,0 +1,55 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import numpy as np
import scipy.signal as spsig
from pathlib import Path
from filelock import FileLock
def pytest_sessionstart():
gen_dir = Path(__file__).parent / "autogen"
gen_dir.mkdir(exist_ok=True, parents=True)
coeffs = np.zeros(1000)
coeffs[0] = 1
out_dir = Path(gen_dir, "passthrough_filter.txt")
with FileLock(str(out_dir) + ".lock"):
if not out_dir.is_file():
np.savetxt(out_dir, coeffs)
coeffs = np.arange(10, 0, -1)/10
out_dir = Path(gen_dir, "descending_coeffs.txt")
with FileLock(str(out_dir) + ".lock"):
if not out_dir.is_file():
np.savetxt(out_dir, coeffs)
coeffs = spsig.firwin2(512, [0.0, 0.5, 1.0], [1.0, 1.0, 0.0])
out_dir = Path(gen_dir,"simple_low_pass.txt")
with FileLock(str(out_dir) + ".lock"):
if not out_dir.is_file():
np.savetxt(out_dir, coeffs)
coeffs = spsig.firwin2(2048, [0.0, 20/48000, 1.0], [0.0, 1.0, 1.0], antisymmetric=True)
out_dir = Path(gen_dir, "aggressive_high_pass.txt")
with FileLock(str(out_dir) + ".lock"):
if not out_dir.is_file():
np.savetxt(out_dir, coeffs)
coeffs = spsig.firwin2(2047, [0.0, 0.5, 1.0], [0.5, 1.0, 2.0])
out_dir = Path(gen_dir, "tilt.txt")
with FileLock(str(out_dir) + ".lock"):
if not out_dir.is_file():
np.savetxt(out_dir, coeffs)
coeffs = np.zeros(10000)
coeffs[::8] = 1
out_dir = Path(gen_dir, "comb.txt")
with FileLock(str(out_dir) + ".lock"):
if not out_dir.is_file():
np.savetxt(out_dir, coeffs)
if __name__ == "__main__":
pytest_sessionstart()

View File

@@ -0,0 +1,64 @@
// Copyright 2024-2025 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include "xmath/filter.h"
FILE * _fopen(char * fname, char* mode) {
FILE * fp = fopen(fname, mode);
if (fp == NULL)
{
printf("Error opening a file\n");
exit(1);
}
return fp;
}
int main()
{
// this is just a simple wrapper for lib_xcore_math's filter_fir_s32,
// for testing bit exactness against the Python model.
right_shift_t rsh = 0;
int32_t n_taps = 0;
FILE * in = _fopen("../sig_48k.bin", "rb");
FILE * out = _fopen("sig_out.bin", "wb");
FILE * coeffs = _fopen("coeffs.bin", "rb");
fseek(in, 0, SEEK_END);
int in_len = ftell(in) / sizeof(int32_t);
fseek(in, 0, SEEK_SET);
fread(&rsh, sizeof(int32_t), 1, coeffs);
fread(&n_taps, sizeof(int32_t), 1, coeffs);
int32_t * taps_buf = calloc(sizeof(int32_t), n_taps);
int32_t * samp_buf = calloc(sizeof(int32_t), n_taps);
fread(taps_buf, sizeof(int32_t), n_taps, coeffs);
//printf("%ld %ld %ld %ld %ld %d\n", taps_buf[0], taps_buf[1], taps_buf[2], taps_buf[3], taps_buf[4], lsh);
fclose(coeffs);
filter_fir_s32_t * filter = malloc(sizeof(filter_fir_s32_t));
filter_fir_s32_init(filter, samp_buf, n_taps, taps_buf, rsh);
for (unsigned i = 0; i < in_len; i++)
{
int32_t samp = 0, samp_out = 0;
fread(&samp, sizeof(int32_t), 1, in);
//printf("%ld ", samp);
samp_out = filter_fir_s32(filter, samp);
//printf("%ld ", samp_out);
fwrite(&samp_out, sizeof(int32_t), 1, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,67 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import numpy as np
import pytest
from pathlib import Path
import audio_dsp.dsp.fir as fir
import audio_dsp.dsp.td_block_fir as tbf
import audio_dsp.dsp.fd_block_fir as fbf
import audio_dsp.dsp.signal_gen as sg
import audio_dsp.dsp.utils as utils
gen_dir = Path(__file__).parent / "autogen"
# Note the filter coeffs files are defined in test/fir/conftest.py
@pytest.mark.parametrize("coeff_path", ["passthrough_filter.txt",
"descending_coeffs.txt",
"simple_low_pass.txt"])
@pytest.mark.parametrize("n_chans", [1, 2, 4])
@pytest.mark.parametrize("block_size", [8])
def test_frames(coeff_path, n_chans, block_size):
fir_d = fir.fir_direct(48000, n_chans, Path(gen_dir, coeff_path))
fir_btd = tbf.fir_block_td(48000, n_chans, Path(gen_dir, coeff_path), "dut",
gen_dir, frame_advance=block_size)
fir_bfd = fbf.fir_block_fd(48000, n_chans, Path(gen_dir, coeff_path), "dut",
gen_dir, block_size, 0, 256)
np.random.seed(0)
signal = sg.pink_noise(48000, 0.01, 0.5)
signal = np.zeros(512)
signal[0] = 1
# signal[30:40] = 1
signal = np.tile(signal, [n_chans, 1])
signal[0] = -signal[0]
frame_size = block_size
signal_frames = utils.frame_signal(signal, frame_size, frame_size)
out_flt_d = np.zeros_like(signal)
out_flt_btd = np.zeros_like(signal)
out_flt_bfd = np.zeros_like(signal)
out_int = np.zeros_like(out_flt_d)
for n in range(len(signal_frames)):
out_flt_d [:, n*frame_size:(n+1)*frame_size] = fir_d.process_frame(signal_frames[n])
out_flt_btd[:, n*frame_size:(n+1)*frame_size] = fir_btd.process_frame(signal_frames[n])
out_flt_bfd[:, n*frame_size:(n+1)*frame_size] = fir_bfd.process_frame(signal_frames[n])
assert np.all(-out_flt_d[0, :] == out_flt_d[1:, :])
np.testing.assert_allclose(out_flt_d, out_flt_btd, atol=2**-56, rtol=2**-42)
np.testing.assert_allclose(out_flt_d, out_flt_bfd, atol=2**-51, rtol=2**-41)
fir_d.reset_state()
fir_btd.reset_state()
for n in range(len(signal_frames)):
out_int[:, n*frame_size:(n+1)*frame_size] = fir_d.process_frame_xcore(signal_frames[n])
for n in range(1, n_chans):
# rounding differences can occur between positive and negative signal
np.testing.assert_allclose(-out_int[0, :], out_int[n, :], atol=(2**(-fir_d.Q_sig + 1)))
if __name__ == "__main__":
test_frames("simple_low_pass.txt", 1, 8)

View File

@@ -0,0 +1,110 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import numpy as np
import soundfile as sf
from pathlib import Path
import shutil
import subprocess
import audio_dsp.dsp.fir as fir
from audio_dsp.dsp.generic import Q_SIG
import audio_dsp.dsp.signal_gen as gen
import pytest
import os
from test.test_utils import xdist_safe_bin_write, float_to_qxx, qxx_to_float, q_convert_flt
bin_dir = Path(__file__).parent / "bin"
gen_dir = Path(__file__).parent / "autogen"
fs = 48000
def get_sig(len=0.05):
sig_fl = gen.log_chirp(fs, len, 0.5)
sig_fl = q_convert_flt(sig_fl, 23, Q_SIG)
sig_int = float_to_qxx(sig_fl)
name = "sig_48k"
sig_path = bin_dir / str(name + ".bin")
xdist_safe_bin_write(sig_int, sig_path)
# wav file does not need to be locked as it is only used for debugging outside pytest
wav_path = gen_dir / str(name + ".wav")
sf.write(wav_path, sig_fl, int(fs), "PCM_24")
return sig_fl
def get_c_wav(dir_name, sim = True):
app = "xsim" if sim else "xrun --io"
run_cmd = app + " " + str(bin_dir / "fir_direct_test.xe")
stdout = subprocess.check_output(run_cmd, cwd = dir_name, shell = True)
#print("run msg:\n", stdout)
sig_bin = dir_name / "sig_out.bin"
assert sig_bin.is_file(), f"Could not find output bin {sig_bin}"
sig_int = np.fromfile(sig_bin, dtype=np.int32)
sig_fl = qxx_to_float(sig_int)
sf.write(gen_dir / "sig_c.wav", sig_fl, fs, "PCM_24")
return sig_fl
def run_py(filt: fir.fir_direct, sig_fl):
out_int = np.zeros(sig_fl.size)
for n in range(sig_fl.size):
out_int[n] = filt.process_xcore(sig_fl[n])
# sf.write(gen_dir / "sig_py_int.wav", out_int, fs, "PCM_24")
return out_int
def single_test(filt, tname, sig_fl):
test_dir = bin_dir / tname
test_dir.mkdir(exist_ok = True, parents = True)
coeffs_arr = np.array(filt.coeffs_int, dtype=np.int32,ndmin=1)
taps_arr = np.array(filt.n_taps, dtype=np.int32, ndmin=1)
shift_arr = np.array(filt.shift, dtype=np.int32, ndmin=1)
filt_info = np.concatenate((shift_arr, taps_arr, coeffs_arr))
filt_info.tofile(test_dir / "coeffs.bin")
out_py_int = run_py(filt, sig_fl)
out_c = get_c_wav(test_dir)
shutil.rmtree(test_dir)
overflow_samples = (np.abs(out_py_int) >= (2**(31-filt.Q_sig) - 1))
np.testing.assert_allclose(out_c[~overflow_samples], out_py_int[~overflow_samples], rtol=0, atol=0)
@pytest.fixture(scope="module")
def in_signal():
bin_dir.mkdir(exist_ok=True, parents=True)
gen_dir.mkdir(exist_ok=True, parents=True)
return get_sig()
# Note the filter coeffs files are defined in test/fir/conftest.py
@pytest.mark.parametrize("coeff_path", ["passthrough_filter.txt",
"descending_coeffs.txt",
"simple_low_pass.txt",
"aggressive_high_pass.txt",
"comb.txt",
"tilt.txt"])
def test_fir_direct_c(in_signal, coeff_path):
# this test compares the Python process_xcore fir implementation
# against the lib_xcore_math version and checks for bit exactness
filt = fir.fir_direct(fs, 1, Path(gen_dir, coeff_path))
filter_name = f"fir_direct_{os.path.splitext(coeff_path)[0]}"
single_test(filt, filter_name, in_signal)
if __name__ =="__main__":
bin_dir.mkdir(exist_ok=True, parents=True)
gen_dir.mkdir(exist_ok=True, parents=True)
sig_fl = get_sig()
test_fir_direct_c(sig_fl, "descending_coeffs.txt")

View File

@@ -0,0 +1,92 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import numpy as np
import pytest
from pathlib import Path
import audio_dsp.dsp.fir as fir
import audio_dsp.dsp.signal_gen as sg
import audio_dsp.dsp.utils as utils
from test.test_utils import q_convert_flt, assert_allclose
gen_dir = Path(__file__).parent / "autogen"
# Note the filter coeffs files are defined in test/fir/conftest.py
@pytest.mark.parametrize("coeff_path", ["passthrough_filter.txt",
"descending_coeffs.txt",
"simple_low_pass.txt",
"aggressive_high_pass.txt",
"comb.txt",
"tilt.txt"])
def test_basic(coeff_path):
fut = fir.fir_direct(48000, 1, Path(gen_dir, coeff_path))
signal = sg.pink_noise(48000, 0.1, 0.5)
signal = q_convert_flt(signal, 23, 27)
# signal = np.zeros(1000)
# signal[0] = 1
coeffs = np.loadtxt(Path(gen_dir, coeff_path))
out_ref = np.convolve(signal, coeffs)[:len(signal)]
out_flt = np.zeros_like(signal)
out_int = np.zeros_like(out_flt)
for n in range(len(signal)):
out_flt[n] = fut.process(signal[n])
fut.reset_state()
for n in range(len(signal)):
out_int[n] = fut.process_xcore(signal[n])
# difference in convolution implementations means flt and ref aren't
# bit exact, especially after saturation!
unsaturated = ((out_ref > float(-(2 ** (31 - fut.Q_sig)))) &
(out_ref < float((2**31 - 1) / 2**fut.Q_sig)))
np.testing.assert_allclose(out_flt[unsaturated], out_ref[unsaturated], atol=2**-52)
# small signals are always going to be ropey due to quantizing, so just check average error of top half
top_half = utils.db(out_flt) > -100
if np.any(top_half):
error_flt = np.abs(utils.db(out_int[top_half])-utils.db(out_flt[top_half]))
mean_error_int = utils.db(np.nanmean(utils.db2gain(error_flt)))
assert mean_error_int < 0.016
np.testing.assert_allclose(out_flt, out_int, atol=2**(-21))
# Note the filter coeffs files are defined in test/fir/conftest.py
@pytest.mark.parametrize("coeff_path", ["passthrough_filter.txt",
"descending_coeffs.txt",
"simple_low_pass.txt"])
@pytest.mark.parametrize("n_chans", [1, 2, 4])
def test_frames(coeff_path, n_chans):
fut = fir.fir_direct(48000, n_chans, Path(gen_dir, coeff_path))
signal = sg.pink_noise(48000, 0.1, 0.5)
signal = np.tile(signal, [n_chans, 1])
signal[0] = -signal[0]
frame_size = 1
signal_frames = utils.frame_signal(signal, frame_size, 1)
out_flt = np.zeros_like(signal)
out_int = np.zeros_like(out_flt)
for n in range(len(signal_frames)):
out_flt[:, n*frame_size:(n+1)*frame_size] = fut.process_frame(signal_frames[n])
assert np.all(-out_flt[0, :] == out_flt[1:, :])
fut.reset_state()
for n in range(len(signal_frames)):
out_int[:, n*frame_size:(n+1)*frame_size] = fut.process_frame_xcore(signal_frames[n])
for n in range(1, n_chans):
# rounding differences can occur between positive and negative signal
assert_allclose(-out_int[0, :], out_int[n, :], atol=(2**(-fut.Q_sig)))
if __name__ =="__main__":
# test_basic("simple_low_pass.txt")
# for n in range(100):
test_frames("simple_low_pass.txt", 2)