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,60 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
project(biquad_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(APP_C_SRCS src/main.c)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()
project(coeffs_alltests)
set(APP_HW_TARGET XCORE-AI-EXPLORER)
set(APP_COMPILER_FLAGS
-O3
-g
-report
-Wall
-Werror
-fxscope
-fcmdline-buffer-bytes=1024)
set(APP_DEPENDENT_MODULES lib_audio_dsp)
set(APP_C_SRCS src/coeffs_alltests.c)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()
project(biquad_slew_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(APP_C_SRCS src/main_slew.c)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()

View File

View File

@@ -0,0 +1,120 @@
// 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 "control/biquad.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 read_int();
float read_float();
enum bq_type {allpass, bandpass, bandstop, bypass, constq, gain, high_shelf,
highpass, linkwitz, low_shelf, lowpass, mute, notch, peaking};
int main(int argc, char* argv[])
{
enum bq_type this_bq = atoi(argv[1]);
int n_inputs = 0;
if (this_bq == allpass || this_bq == bandpass || this_bq == bandstop || this_bq == highpass ||
this_bq == lowpass || this_bq == notch){
n_inputs = 3;
}
else if (this_bq == constq || this_bq == high_shelf || this_bq == low_shelf || this_bq == peaking)
{
n_inputs = 4;
}
else if (this_bq == bypass || this_bq == gain || this_bq == mute)
{
n_inputs = 1;
}
else if (this_bq == linkwitz){
n_inputs = 5;
}
else {
printf("Unknown biquad type\n");
exit(1);
}
FILE * in = _fopen("test_vector.bin", "rb");
FILE * out = _fopen("out_vector.bin", "wb");
fseek(in, 0, SEEK_END);
int in_len = ftell(in) / (n_inputs*sizeof(float));
printf("inlen %d", in_len);
fseek(in, 0, SEEK_SET);
for (unsigned i = 0; i < in_len; i++)
{
float samp[5] = {0}; // never more than 4 inputs
fread(&samp, sizeof(float), n_inputs, in);
q2_30 coeffs[5] = {0};
left_shift_t bsh;
if (this_bq == allpass){
bsh = adsp_design_biquad_allpass(coeffs, samp[0], samp[1], samp[2]);
}
else if (this_bq == bandpass){
bsh = adsp_design_biquad_bandpass(coeffs, samp[0], samp[1], samp[2]);
}
else if (this_bq == bandstop){
bsh = adsp_design_biquad_bandstop(coeffs, samp[0], samp[1], samp[2]);
}
else if (this_bq == bypass){
bsh = adsp_design_biquad_bypass(coeffs);
}
else if (this_bq == constq){
bsh = adsp_design_biquad_const_q(coeffs, samp[0], samp[1], samp[2], samp[3]);
}
else if (this_bq == gain){
bsh = adsp_design_biquad_gain(coeffs, samp[0]);
}
else if (this_bq == high_shelf){
bsh = adsp_design_biquad_highshelf(coeffs, samp[0], samp[1], samp[2], samp[3]);
}
else if (this_bq == highpass){
bsh = adsp_design_biquad_highpass(coeffs, samp[0], samp[1], samp[2]);
}
else if (this_bq == linkwitz){
bsh = adsp_design_biquad_linkwitz(coeffs, samp[0], samp[1], samp[2], samp[3], samp[4]);
}
else if (this_bq == lowpass){
bsh = adsp_design_biquad_lowpass(coeffs, samp[0], samp[1], samp[2]);
}
else if (this_bq == low_shelf){
bsh = adsp_design_biquad_lowshelf(coeffs, samp[0], samp[1], samp[2], samp[3]);
}
else if (this_bq == mute){
bsh = adsp_design_biquad_mute(coeffs);
}
else if (this_bq == notch){
bsh = adsp_design_biquad_notch(coeffs, samp[0], samp[1], samp[2]);
}
else if (this_bq == peaking){
bsh = adsp_design_biquad_peaking(coeffs, samp[0], samp[1], samp[2], samp[3]);
}
else {
printf("Unknown biquad type\n");
exit(1);
}
fwrite(&coeffs, sizeof(int32_t), 5, out);
fwrite(&bsh, sizeof(int32_t), 1, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,52 @@
// 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 "dsp/adsp.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()
{
int32_t DWORD_ALIGNED taps_buf[5] = {0};
int32_t state[8] = {0};
left_shift_t lsh = 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(taps_buf, sizeof(int32_t), 5, coeffs);
fread(&lsh, sizeof(int32_t), 1, 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);
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 = adsp_biquad(samp, taps_buf, state, lsh);
//printf("%ld ", samp_out);
fwrite(&samp_out, sizeof(int32_t), 1, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,79 @@
// 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 "dsp/adsp.h"
#include "control/biquad.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()
{
int32_t DWORD_ALIGNED taps_buf[8] = {0};
int32_t DWORD_ALIGNED taps_buf_2[8] = {0};
int32_t DWORD_ALIGNED state[8] = {0};
left_shift_t lsh = 0;
left_shift_t lsh_2 = 0;
int32_t shift = 0;
FILE * in = _fopen("../slew_sig_48k.bin", "rb");
FILE * out = _fopen("sig_out.bin", "wb");
FILE * coeffs = _fopen("coeffs.bin", "rb");
FILE * coeffs_2 = _fopen("coeffs_2.bin", "rb");
fseek(in, 0, SEEK_END);
int in_len = ftell(in) / sizeof(int32_t);
fseek(in, 0, SEEK_SET);
fread(taps_buf, sizeof(int32_t), 5, coeffs);
fread(&lsh, sizeof(int32_t), 1, coeffs);
fread(&shift, sizeof(int32_t), 1, coeffs);
fclose(coeffs);
fread(taps_buf_2, sizeof(int32_t), 5, coeffs_2);
fread(&lsh_2, sizeof(int32_t), 1, coeffs_2);
fclose(coeffs_2);
biquad_slew_t slew_state = adsp_biquad_slew_init(taps_buf, lsh, shift);
int32_t * states[1] = {&state[0]};
for (unsigned i = 0; i < in_len/2; i++)
{
adsp_biquad_slew_coeffs(&slew_state, states, 1);
int32_t samp = 0, samp_out = 0;
fread(&samp, sizeof(int32_t), 1, in);
//printf("%ld ", samp);
samp_out = adsp_biquad(samp, slew_state.active_coeffs, state, slew_state.lsh);
// printf("%ld ", samp_out);
fwrite(&samp_out, sizeof(int32_t), 1, out);
}
adsp_biquad_slew_update_coeffs(&slew_state, states, 1, taps_buf_2, lsh_2);
for (unsigned i = in_len/2; i < in_len; i++)
{
adsp_biquad_slew_coeffs(&slew_state, states, 1);
int32_t samp = 0, samp_out = 0;
fread(&samp, sizeof(int32_t), 1, in);
//printf("%ld ", samp);
samp_out = adsp_biquad(samp, slew_state.active_coeffs, state, slew_state.lsh);
// printf("%ld ", samp_out);
fwrite(&samp_out, sizeof(int32_t), 1, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,161 @@
# 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.biquad as bq
from audio_dsp.dsp.generic import Q_SIG
import audio_dsp.dsp.signal_gen as gen
import pytest
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 / "biquad_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: bq.biquad, 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.int_coeffs, dtype=np.int32)
shift_arr = np.array(filt.b_shift, dtype=np.int32)
filt_info = np.append(coeffs_arr, shift_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)
np.testing.assert_allclose(out_c, out_py_int, 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()
@pytest.mark.parametrize("filter_type", ["biquad_lowpass",
"biquad_highpass",
"biquad_notch",
"biquad_allpass"])
@pytest.mark.parametrize("f", [20, 20000])
@pytest.mark.parametrize("q", [0.1, 10])
def test_xpass_filters_c(in_signal, filter_type, f, q):
f = np.min([f, fs / 2 * 0.95])
filter_handle = getattr(bq, "make_%s" % filter_type)
filt = bq.biquad(filter_handle(fs, f, q), fs, 1)
filter_name = f"{filter_type}_{f}_{q}"
single_test(filt, filter_name, in_signal)
@pytest.mark.parametrize("filter_type", ["biquad_peaking",
"biquad_constant_q",
"biquad_lowshelf",
"biquad_highshelf",])
@pytest.mark.parametrize("f", [20, 20000])
@pytest.mark.parametrize("q", [0.1, 10])
@pytest.mark.parametrize("gain", [-12, 12])
def test_high_gain_c(in_signal, filter_type, f, q, gain):
f = np.min([f, fs / 2 * 0.95])
filter_handle = getattr(bq, "make_%s" % filter_type)
filt = bq.biquad(filter_handle(fs, f, q, gain), fs, 1)
filter_name = f"{filter_type}_{f}_{q}_{gain}"
single_test(filt, filter_name, in_signal)
@pytest.mark.parametrize("filter_type", ["biquad_bandpass",
"biquad_bandstop",])
@pytest.mark.parametrize("f", [20, 20000])
@pytest.mark.parametrize("q", [0.1, 10])
def test_bandx_filters_c(in_signal, filter_type, f, q):
f = np.min([f, fs / 2 * 0.95])
high_q_stability_limit = 0.85
if q >= 5 and f / (fs / 2) > high_q_stability_limit:
f = high_q_stability_limit * fs / 2
filter_handle = getattr(bq, "make_%s" % filter_type)
filt = bq.biquad(filter_handle(fs, f, q), fs, 1)
filter_name = f"{filter_type}_{f}_{q}"
single_test(filt, filter_name, in_signal)
@pytest.mark.parametrize("f0", [20, 100, 500])
@pytest.mark.parametrize("fp_ratio", [0.4, 4])
@pytest.mark.parametrize("q0, qp", [(0.5, 2), (2, 0.5), (0.707, 0.707)])
def test_linkwitz_filters_c(in_signal, f0, fp_ratio, q0, qp):
fp = f0*fp_ratio
filt = bq.biquad(bq.make_biquad_linkwitz(fs, f0, q0, f0*fp_ratio, qp), fs, 1)
filter_name = f"biquad_linkwitz_{f0}_{fp_ratio}_{q0}_{qp}"
single_test(filt, filter_name, in_signal)
@pytest.mark.parametrize("gain", [-10, 0, 10])
def test_gain_filters_c(in_signal, gain):
filt = bq.biquad(bq.make_biquad_gain(fs, gain), fs, 1)
filter_name = f"biquad_gain_{gain}"
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_xpass_filters_c(sig_fl, "biquad_notch", 200, 0.7)
#test_high_gain_c(sig_fl, "biquad_lowshelf", 2000, 0.1, 5)
test_bandx_filters_c(sig_fl, "biquad_bandpass", 200, 10)
#test_linkwitz_filters_c(sig_fl, 100, 4, 0.5, 2)
# test_gain_filters_c(sig_fl, -10)

View File

@@ -0,0 +1,503 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""
Test python vs C biquad coeff generators
Write sets of params to file (usually 3 params), then read into C.
For each parameter set, write the 5 coefficients to file from C.
Read the sets of 5 coeffs into python, compare against python implementation.
"""
import numpy as np
from pathlib import Path
import subprocess
import itertools
import time
from enum import IntEnum
import audio_dsp.dsp.biquad as bq
from audio_dsp.dsp.generic import Q_SIG
from test.test_utils import assert_allclose
bin_dir = Path(__file__).parent / "bin"
gen_dir = Path(__file__).parent / "autogen"
fs=48000
def flt_to_bin_file(sig_fl, out_dir=bin_dir):
sig_fl32 = np.array(sig_fl).astype(np.float32)
name = "test_vector"
sig_fl32.tofile(out_dir / f"{name}.bin")
# time.sleep(1)
return sig_fl32
class bq_type(IntEnum):
allpass = 0
bandpass = 1
bandstop = 2
bypass = 3
constq = 4
gain = 5
high_shelf = 6
highpass = 7
linkwitz = 8
low_shelf = 9
lowpass = 10
mute = 11
notch = 12
peaking = 13
def convert_coeffs(flt_coeffs):
out_coeffs = np.zeros(6)
b_shift = bq._get_bshift(np.float32(flt_coeffs))
_, out_coeffs[:5] = bq._round_and_check(flt_coeffs, b_shift)
out_coeffs[5] = b_shift
return out_coeffs
def b_shift_coeffs(coeffs_python, out_c):
n_sets = coeffs_python.shape[0]
shifted_py = np.zeros((n_sets, 5), dtype=int)
shifted_c = np.zeros((n_sets, 5), dtype=int)
# apply b shifts to coeffs
for n in range(n_sets):
shifted_c[n, :3] = np.int64(out_c[n, :3]) << out_c[n, 5]
shifted_c[n, 3:] = out_c[n, 3:5]
shifted_py[n, :3] = np.int64(coeffs_python[n, :3]) << coeffs_python[n, 5]
shifted_py[n, 3:] = coeffs_python[n, 3:5]
return shifted_py, shifted_c
def get_c_wav(dir_name, conv_name, verbose=True, sim = True, dtype=np.float32):
# get the enum from the name, assuming name starts with "coeffs_..."
bq_name = conv_name[7:]
enum = bq_type[bq_name].value
app = "xsim" if sim else "xrun --io"
run_cmd = app + " --args " + str(bin_dir / "coeffs_alltests.xe") + f" {enum}"
stdout = subprocess.check_output(run_cmd, cwd = dir_name, shell = True)
if verbose: print("run msg:\n", stdout.decode())
sig_bin = dir_name / "out_vector.bin"
assert sig_bin.is_file(), f"Could not find output bin {sig_bin}"
sig_int = np.fromfile(sig_bin, dtype=dtype)
return sig_int
def test_design_biquad_bypass():
test_dir = bin_dir / "coeffs_bypass"
test_dir.mkdir(exist_ok = True, parents = True)
ratios = [[5]]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_bypass", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_bypass(48000)
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-16, atol=0)
def test_design_biquad_mute():
test_dir = bin_dir / "coeffs_mute"
test_dir.mkdir(exist_ok = True, parents = True)
ratios = [[5]]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_mute", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_mute(48000)
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-16, atol=0)
def test_design_biquad_gain():
test_dir = bin_dir / "coeffs_gain"
test_dir.mkdir(exist_ok = True, parents = True)
ratios = [[10], [0], [-10], [-200]]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_gain", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_gain(48000, ratios[n][0])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-16, atol=0)
def test_design_biquad_lowpass():
test_dir = bin_dir / "coeffs_lowpass"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_lowpass", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_lowpass(ratios[n][1], ratios[n][0], ratios[n][2])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-19, atol=1)
def test_design_biquad_highpass():
test_dir = bin_dir / "coeffs_highpass"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_highpass", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_highpass(ratios[n][1], ratios[n][0], ratios[n][2])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-19, atol=0)
def bandx_param_check(ratios):
new_ratios = []
for ratio in ratios:
f, fs, q = ratio
if f < fs*1e-3:
q = max(0.5, q)
high_q_stability_limit = 0.85
if q >= 5 and f/(fs/2) > high_q_stability_limit:
f = high_q_stability_limit*fs/2
new_ratios.append([f, fs, q])
return new_ratios
def test_design_biquad_bandpass():
test_dir = bin_dir / "coeffs_bandpass"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
ratios = bandx_param_check(ratios)
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_bandpass", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_bandpass(ratios[n][1], ratios[n][0], ratios[n][2])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-17, atol=1)
def test_design_biquad_bandstop():
test_dir = bin_dir / "coeffs_bandstop"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
ratios = bandx_param_check(ratios)
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_bandstop", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_bandstop(ratios[n][1], ratios[n][0], ratios[n][2])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-17, atol=1)
def test_design_biquad_notch():
test_dir = bin_dir / "coeffs_notch"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_notch", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_notch(ratios[n][1], ratios[n][0], ratios[n][2])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-19, atol=0)
def test_design_biquad_allpass():
test_dir = bin_dir / "coeffs_allpass"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_allpass", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_allpass(ratios[n][1], ratios[n][0], ratios[n][2])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-19, atol=0)
def test_design_biquad_peaking():
test_dir = bin_dir / "coeffs_peaking"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
gain = [-12, -6, 0, 6, 18, 19]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q, gain))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_peaking", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_peaking(ratios[n][1], ratios[n][0], ratios[n][2], ratios[n][3])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-13, atol=0)
def test_design_biquad_constq():
test_dir = bin_dir / "coeffs_constq"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2, 5, 10]
gain = [-40, -12, -6, 0, 6, 18, 19]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q, gain))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_constq", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_constant_q(ratios[n][1], ratios[n][0], ratios[n][2], ratios[n][3])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-12, atol=0)
def test_design_biquad_high_shelf():
test_dir = bin_dir / "coeffs_high_shelf"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2]
gain = [-12, -6, 0, 6, 12, 13]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q, gain))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_high_shelf", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_highshelf(ratios[n][1], ratios[n][0], ratios[n][2], ratios[n][3])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-11.2, atol=0)
def test_design_biquad_low_shelf():
test_dir = bin_dir / "coeffs_low_shelf"
test_dir.mkdir(exist_ok = True, parents = True)
f = [20, 100, 1000, 10000, 20000]
q = [0.1, 0.5, 1, 2]
gain = [-12, -6, 0, 6, 12, 13]
fs = [16000, 44100, 48000, 88200, 96000, 192000]
ratios = list(itertools.product(f, fs, q, gain))
# get rid of f >= fs/2
ratios = [ratio for ratio in ratios if ratio[0] < (ratio[1]/2)]
flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_low_shelf", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_lowshelf(ratios[n][1], ratios[n][0], ratios[n][2], ratios[n][3])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-11.2, atol=1)
def test_design_biquad_linkwitz():
test_dir = bin_dir / "coeffs_linkwitz"
test_dir.mkdir(exist_ok = True, parents = True)
fs = [16000, 44100, 48000, 88200, 96000, 192000]
f0 = [20, 50, 100, 200]
fp_ratio = [0.4, 1, 4]
q0 = [0.5, 2, 0.707]
qp = [0.5, 2, 0.707]
initialratios = list(itertools.product(f0, fs, q0, fp_ratio, qp))
ratios = []
for ratio in initialratios:
if ratio[1] > 100000 and ratio[0] < 50 and ratio[3] < 1:
ratio_0 = 30
else: ratio_0 = ratio[0]
ratios.append([ratio_0, ratio[1], ratio[2], ratio[3]*ratio_0, ratio[4]])
ratios_32 = flt_to_bin_file(ratios, test_dir)
out_c = get_c_wav(test_dir, "coeffs_linkwitz", dtype=np.int32)
out_c = np.reshape(out_c, newshape=[-1, 6])
coeffs_python = np.zeros((len(ratios), 6), dtype=np.int32)
for n in range(len(ratios)):
flt_coeffs = bq.make_biquad_linkwitz(ratios[n][1], ratios[n][0], ratios[n][2], ratios[n][3], ratios[n][4])
coeffs_python[n] = convert_coeffs(flt_coeffs)
shifted_py, shifted_c = b_shift_coeffs(coeffs_python, out_c)
# this doesn't work if one of the coefficients is zero
assert_allclose(shifted_c, shifted_py, rtol=2**-22, atol=0)
if __name__ == "__main__":
bin_dir.mkdir(exist_ok=True, parents=True)
gen_dir.mkdir(exist_ok=True, parents=True)
test_design_biquad_constq()

View File

@@ -0,0 +1,330 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
import pytest
import numpy as np
import audio_dsp.dsp.biquad as bq
import audio_dsp.dsp.signal_gen as gen
import audio_dsp.dsp.utils as utils
def saturation_test(filter: bq.biquad, fs):
signal = 2**(np.arange(0, 31.5, 0.5)) - 1
signal = np.repeat(signal, 2)
signal[::2] *= -1
# # used for lib_xcore_math biquad test
# sigint = (np.round(signal).astype(np.int32))
# np.savetxt("sig.csv", sigint, fmt="%i", delimiter=",")
signal = 2.0**30 - 1
signal = np.repeat(signal, 4)
signal *= (2**-31)
output_int = np.zeros(len(signal))
output_flt = np.zeros(len(signal))
output_vpu = np.zeros(len(signal))
# for n in np.arange(len(signal)):
# output_int[n] = filter.process_int(signal[n])
# filter.reset_state()
# for n in np.arange(len(signal)):
# output_flt[n] = filter.process(signal[n])
filter.reset_state()
for n in np.arange(len(signal)):
output_vpu[n] = filter.process_xcore(signal[n])
# # reference result for lib_xcore_math test
# vpu_int = (np.round(output_vpu * 2**31).astype(np.int32))
# np.savetxt("out.csv", vpu_int, fmt="%i", delimiter=",")
# small signals are always going to be ropey due to quantizing, so just check average error of top half
top_half = utils.db(output_flt) > -50
if np.any(top_half):
error_flt = np.abs(utils.db(output_int[top_half])-utils.db(output_flt[top_half]))
mean_error_flt = np.abs(utils.db(np.nanmean(utils.db2gain(error_flt))))
assert mean_error_flt < 0.055
error_vpu = np.abs(utils.db(output_int[top_half])-utils.db(output_vpu[top_half]))
mean_error_vpu = np.abs(utils.db(np.nanmean(utils.db2gain(error_vpu))))
assert mean_error_vpu < 0.05
def chirp_filter_test(filter: bq.biquad, fs):
length = 0.05
signal = gen.log_chirp(fs, length, 1.0)
output_int = np.zeros(len(signal))
output_flt = np.zeros(len(signal))
output_vpu = np.zeros(len(signal))
for n in np.arange(len(signal)):
output_int[n] = filter.process_int(signal[n])
filter.reset_state()
for n in np.arange(len(signal)):
output_flt[n] = filter.process(signal[n])
filter.reset_state()
for n in np.arange(len(signal)):
output_vpu[n] = filter.process_xcore(signal[n])
# small signals are always going to be ropey due to quantizing, so just check average error of top half
top_half = utils.db(output_flt) > -50
# after saturation, the implementations diverge, but they should
# initially saturate at the same sample
if output_flt.max() > 2**(31-filter.Q_sig):
first_sat = np.argmax(np.abs(output_flt) >= 2**(31-filter.Q_sig))
top_half[first_sat + 1:] = False
if np.any(top_half):
error_flt = np.abs(utils.db(output_int[top_half])-utils.db(output_flt[top_half]))
mean_error_flt = np.abs(utils.db(np.nanmean(utils.db2gain(error_flt))))
assert mean_error_flt < 0.055
error_vpu = np.abs(utils.db(output_int[top_half])-utils.db(output_vpu[top_half]))
mean_error_vpu = np.abs(utils.db(np.nanmean(utils.db2gain(error_vpu))))
assert mean_error_vpu < 0.05
def test_4_coeff_overflow():
fs = 48000
filter = bq.biquad([1.0, -1.5, 0.5625, 1.5, -0.5625], fs, Q_sig=31)
saturation_test(filter, 48000)
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
@pytest.mark.parametrize("amplitude", [0.5, 1, 2, 16])
def test_bypass(fs, amplitude):
filter = bq.biquad(bq.make_biquad_bypass(fs), fs, 1)
length = 0.05
signal = gen.log_chirp(fs, length, amplitude)
signal = utils.saturate_float_array(signal, filter.Q_sig)
output_int = np.zeros(len(signal))
output_flt = np.zeros(len(signal))
output_xcore = np.zeros(len(signal))
for n in np.arange(len(signal)):
output_int[n] = filter.process_int(signal[n])
filter.reset_state()
for n in np.arange(len(signal)):
output_flt[n] = filter.process(signal[n])
filter.reset_state()
for n in np.arange(len(signal)):
output_xcore[n] = filter.process_xcore(signal[n])
np.testing.assert_array_equal(signal, output_flt)
np.testing.assert_allclose(signal, output_int, atol=2**-31)
np.testing.assert_allclose(signal, output_xcore, atol=2**-31)
@pytest.mark.parametrize("filter_type", ["biquad_peaking",
"biquad_constant_q"])
@pytest.mark.parametrize("f", [20, 100, 1000, 10000, 20000])
@pytest.mark.parametrize("q", [0.1, 0.5, 1, 2, 10])
@pytest.mark.parametrize("gain", [-12, -6, 0, 6, 12])
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
def test_peaking_filters(filter_type, f, q, gain, fs):
if f < fs*5e-4:
f = max(fs*5e-4, f)
filter_handle = getattr(bq, "make_%s" % filter_type)
filter = bq.biquad(filter_handle(fs, np.min([f, fs/2*0.95]), q, gain), fs)
chirp_filter_test(filter, fs)
@pytest.mark.parametrize("filter_type", ["biquad_lowshelf",
"biquad_highshelf",])
@pytest.mark.parametrize("f", [20, 100, 1000, 10000, 20000])
@pytest.mark.parametrize("q", [0.1, 0.5, 1, 2])
@pytest.mark.parametrize("gain", [-12, -6, 0, 6, 12])
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
def test_shelf_filters(filter_type, f, q, gain, fs):
if f < fs*5e-4:
f = max(fs*5e-4, f)
filter_handle = getattr(bq, "make_%s" % filter_type)
filter = bq.biquad(filter_handle(fs, np.min([f, fs/2*0.95]), q, gain), fs)
chirp_filter_test(filter, fs)
@pytest.mark.parametrize("filter_type", ["biquad_lowpass",
"biquad_highpass",
"biquad_notch",
"biquad_allpass"])
@pytest.mark.parametrize("f", [20, 100, 1000, 10000, 20000])
@pytest.mark.parametrize("q", [0.1, 0.5, 1, 2, 5, 10])
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
def test_xpass_filters(filter_type, f, q, fs):
if f < fs*5e-4 and filter_type == "biquad_lowpass":
f = max(fs*5e-4, f)
filter_handle = getattr(bq, "make_%s" % filter_type)
filter = bq.biquad(filter_handle(fs, np.min([f, fs/2*0.95]), q), fs)
chirp_filter_test(filter, fs)
@pytest.mark.parametrize("filter_type", ["biquad_bandpass",
"biquad_bandstop",])
@pytest.mark.parametrize("f", [100, 1000, 10000, 20000])
@pytest.mark.parametrize("q", [0.1, 0.5, 1, 2, 5, 10])
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
def test_bandx_filters(filter_type, f, q, fs):
filter_handle = getattr(bq, "make_%s" % filter_type)
f = np.min([f, fs/2*0.95])
if f < fs*1e-3:
q = max(0.5, q)
high_q_stability_limit = 0.85
if q >= 5 and f/(fs/2) > high_q_stability_limit:
f = high_q_stability_limit*fs/2
filter = bq.biquad(filter_handle(fs, f, q), fs)
chirp_filter_test(filter, fs)
@pytest.mark.parametrize("f0,", [20, 50, 100, 200])
@pytest.mark.parametrize("fp_ratio", [0.4, 1, 4])
@pytest.mark.parametrize("q0, qp", [(0.5, 2),
(2, 0.5),
(0.707, 0.707)])
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
def test_linkwitz_filters(f0, fp_ratio, q0, qp, fs):
if fs > 100000 and f0 < 50 and fp_ratio < 1:
f0 = 30
filter = bq.biquad(bq.make_biquad_linkwitz(fs, f0, q0, f0*fp_ratio, qp), fs, 1)
chirp_filter_test(filter, fs)
@pytest.mark.parametrize("gain", [-10, 0, 10])
@pytest.mark.parametrize("fs", [16000, 44100, 48000, 88200, 96000, 192000])
def test_gain_filters(gain, fs):
filter = bq.biquad(bq.make_biquad_gain(fs, gain), fs, 1)
chirp_filter_test(filter, fs)
@pytest.mark.parametrize("fs", [48000])
@pytest.mark.parametrize("filter_n", np.arange(9))
@pytest.mark.parametrize("n_chans", [1, 2, 4])
@pytest.mark.parametrize("q_format", [27, 31])
def test_frames(filter_n, fs, n_chans, q_format):
filter_spec = [['lowpass', fs*0.4, 0.707],
['highpass', fs*0.001, 1],
['peaking', fs*1000/48000, 5, 10],
['constant_q', fs*500/48000, 1, -10],
['notch', fs*2000/48000, 1],
['lowshelf', fs*200/48000, 1, 3],
['highshelf', fs*5000/48000, 1, -2],
['bypass'],
['gain', -2]]
filter_spec = filter_spec[filter_n]
filter_handle = getattr(bq, "make_biquad_%s" % filter_spec[0])
filter = bq.biquad(filter_handle(fs, *filter_spec[1:]), fs, n_chans, Q_sig=q_format)
length = 0.05
signal = gen.log_chirp(fs, length, 0.5)
signal = np.tile(signal, [n_chans, 1])
signal_frames = utils.frame_signal(signal, 1, 1)
output_int = np.zeros_like(signal)
output_flt = np.zeros_like(signal)
output_vpu = np.zeros_like(signal)
frame_size = 1
for n in range(len(signal_frames)):
output_int[:, n*frame_size:(n+1)*frame_size] = filter.process_frame_int(signal_frames[n])
assert np.all(output_int[0, :] == output_int)
filter.reset_state()
for n in range(len(signal_frames)):
output_flt[:, n*frame_size:(n+1)*frame_size] = filter.process_frame(signal_frames[n])
assert np.all(output_flt[0, :] == output_flt)
filter.reset_state()
for n in range(len(signal_frames)):
output_vpu[:, n*frame_size:(n+1)*frame_size] = filter.process_frame_xcore(signal_frames[n])
assert np.all(output_vpu[0, :] == output_vpu)
def test_coeff_change():
fs = 48000
coeffs_1 = bq.make_biquad_constant_q(fs, 100, 8, -10)
coeffs_2 = bq.make_biquad_constant_q(fs, 10000, 8, -10)
bq_1 = bq.biquad(coeffs_1, fs, 1)
bq_2 = bq.biquad(coeffs_1, fs, 1)
bq_3 = bq.biquad_slew(coeffs_1, fs, 1, slew_shift=6)
bq_4 = bq.biquad_slew(coeffs_1, fs, 1, slew_shift=6)
amplitude = 0.1
dc = 0
signal = gen.sin(fs, 0.2, 10000, amplitude) + dc
output_flt_reset = np.zeros_like(signal)
output_vpu_reset = np.zeros_like(signal)
output_flt_slew = np.zeros_like(signal)
output_vpu_slew = np.zeros_like(signal)
for n in range(2000):
output_flt_reset[n] = bq_1.process(signal[n])
output_vpu_reset[n] = bq_2.process_xcore(signal[n])
output_flt_slew[n] = bq_3.process_channels([signal[n]])[0]
output_vpu_slew[n] = bq_4.process_channels_xcore([signal[n]])[0]
bq_1.update_coeffs(coeffs_2)
bq_2.update_coeffs(coeffs_2)
bq_3.update_coeffs(coeffs_2)
bq_4.update_coeffs(coeffs_2)
for n in range(2000, 5000):
output_flt_reset[n] = bq_1.process(signal[n])
output_vpu_reset[n] = bq_2.process_xcore(signal[n])
output_flt_slew[n] = bq_3.process_channels([signal[n]])[0]
output_vpu_slew[n] = bq_4.process_channels_xcore([signal[n]])[0]
bq_1.update_coeffs(coeffs_1)
bq_2.update_coeffs(coeffs_1)
bq_3.update_coeffs(coeffs_1)
bq_4.update_coeffs(coeffs_1)
for n in range(5000, len(signal)):
output_flt_reset[n] = bq_1.process(signal[n])
output_vpu_reset[n] = bq_2.process_xcore(signal[n])
output_flt_slew[n] = bq_3.process_channels([signal[n]])[0]
output_vpu_slew[n] = bq_4.process_channels_xcore([signal[n]])[0]
assert np.max(np.abs(output_flt_reset - dc)) < amplitude*1.01
assert np.max(np.abs(output_vpu_reset - dc)) < amplitude*1.01
assert np.max(np.abs(output_flt_slew - dc)) < amplitude*1.01
assert np.max(np.abs(output_vpu_slew - dc)) < amplitude*1.01
top_half = utils.db(output_flt_reset) > -50
if np.any(top_half):
error_vpu = np.abs(utils.db(output_flt_reset[top_half])-utils.db(output_vpu_reset[top_half]))
mean_error_vpu = np.abs(utils.db(np.nanmean(utils.db2gain(error_vpu))))
assert mean_error_vpu < 0.05
top_half = utils.db(output_flt_slew) > -50
if np.any(top_half):
error_vpu = np.abs(utils.db(output_flt_slew[top_half])-utils.db(output_vpu_slew[top_half]))
mean_error_vpu = np.abs(utils.db(np.nanmean(utils.db2gain(error_vpu))))
assert mean_error_vpu < 0.05
pass
# TODO check biquad actually filters
# TODO check parameter generation
# TODO check sample rates - use f/fs
# TODO add mute tests
if __name__ == "__main__":
# test_linkwitz_filters(500, 2, 20, 0.5, 48000)
# test_bandx_filters("biquad_bandstop", 10000, 10, 16000)
# test_bypass(96000, 1)
# test_gain_filters(5, 16000)
# test_peaking_filters("biquad_peaking", 20, 0.5, 12, 16000)
test_coeff_change()

View File

@@ -0,0 +1,125 @@
# Copyright 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.biquad as bq
from audio_dsp.dsp.generic import Q_SIG
import audio_dsp.dsp.signal_gen as gen
import pytest
from test.test_utils import xdist_safe_bin_write, float_to_qxx, qxx_to_float, q_convert_flt
import os
bin_dir = Path(__file__).parent / "bin"
gen_dir = Path(__file__).parent / "autogen"
fs = 48000
def get_c_slew_wav(dir_name, sim = True):
app = "xsim" if sim else "xrun --io"
run_cmd = app + " " + str(bin_dir / "biquad_slew_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_slew(filt: bq.biquad_slew, sig_fl, coeffs_2):
out_int = np.zeros(sig_fl.size)
for n in range(sig_fl.size//2):
out_int[n] = filt.process_channels_xcore([sig_fl[n]])[0]
filt.update_coeffs(coeffs_2)
for n in range(sig_fl.size//2, sig_fl.size):
out_int[n] = filt.process_channels_xcore([sig_fl[n]])[0]
sf.write(gen_dir / "sig_py_int.wav", out_int, fs, "PCM_24")
return out_int
def single_slew_test(filt, tname, sig_fl, filt_2, coeffs_2):
test_dir = bin_dir / tname
test_dir.mkdir(exist_ok = True, parents = True)
coeffs_arr = np.array(filt.int_coeffs, dtype=np.int32)
shift_arr = np.array(filt.b_shift, dtype=np.int32)
slew_arr = np.array(filt.slew_shift, dtype=np.int32)
filt_info = np.append(coeffs_arr, (shift_arr, slew_arr))
filt_info.tofile(test_dir / "coeffs.bin")
coeffs_arr = np.array(filt_2.int_coeffs, dtype=np.int32)
shift_arr = np.array(filt_2.b_shift, dtype=np.int32)
filt_2_info = np.append(coeffs_arr, shift_arr)
filt_2_info.tofile(test_dir / "coeffs_2.bin")
out_py_int = run_py_slew(filt, sig_fl, coeffs_2)
out_c = get_c_slew_wav(test_dir)
shutil.rmtree(test_dir)
np.testing.assert_allclose(out_c, out_py_int, rtol=0, atol=0)
@pytest.mark.parametrize("filter_1", [["biquad_constant_q", 100, 8, -10], ["biquad_gain", 0], ["biquad_constant_q", 10000, 8, -10], ["biquad_gain", -10], ["biquad_highshelf", 1000, 1, 10], ["biquad_highshelf", 1000, 1, -6], ["biquad_peaking", 1000, 0.1, 20], ["biquad_highpass", 1000, 1]])
@pytest.mark.parametrize("filter_2", [["biquad_constant_q", 100, 8, -10], ["biquad_gain", 0], ["biquad_constant_q", 10000, 8, -10], ["biquad_gain", -10], ["biquad_highshelf", 1000, 1, 10], ["biquad_highshelf", 1000, 1, -6], ["biquad_peaking", 1000, 0.1, 20], ["biquad_highpass", 1000, 1]])
@pytest.mark.parametrize("slew_shift", [6])
def test_slew_c(in_signal, filter_1, filter_2, slew_shift):
print(filter_1)
print(filter_2)
filter_type_1 = filter_1[0]
filter_type_2 = filter_2[0]
coeffs_hand_1 = getattr(bq, f"make_{filter_type_1}")
coeffs_1 = coeffs_hand_1(fs, *filter_1[1:])
coeffs_hand_2 = getattr(bq, f"make_{filter_type_2}")
coeffs_2 = coeffs_hand_2(fs, *filter_2[1:])
filt =bq.biquad_slew(coeffs_1, fs, 1, slew_shift=slew_shift)
filt2 =bq.biquad_slew(coeffs_2, fs, 1, slew_shift=slew_shift)
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
filter_name = f"slew_{worker_id}_{filter_type_1}_{filter_type_2}"
single_slew_test(filt, filter_name, in_signal, filt2, coeffs_2)
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_fl = np.ones(int(len*fs))*0.5
sig_int = float_to_qxx(sig_fl)
name = "slew_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
@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()
if __name__ == "__main__":
bin_dir.mkdir(exist_ok=True, parents=True)
gen_dir.mkdir(exist_ok=True, parents=True)
sig_fl = get_sig()
test_slew_c(sig_fl, ["biquad_constant_q", 100, 8, -10], ["biquad_constant_q", 10000, 8, -10], 6)