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,78 @@
cmake_minimum_required(VERSION 3.21)
include($ENV{XMOS_CMAKE_PATH}/xcommon.cmake)
set(CMAKE_OBJECT_PATH_MAX 128)
set(CMAKE_OBJECT_NAME_MAX 1)
project(reverb_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/reverb_room.c)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()
project(reverb_st_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/reverb_room_st.c)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()
project(reverb_plate_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/reverb_plate.c)
set(XMOS_SANDBOX_DIR ${CMAKE_SOURCE_DIR}/../../..)
XMOS_REGISTER_APP()
project(reverb_converters)
set(APP_C_SRCS src/converters.c)
set(APP_COMPILER_FLAGS_FLOAT2INT ${APP_COMPILER_FLAGS} -DFLOAT2INT)
set(APP_COMPILER_FLAGS_DB2INT ${APP_COMPILER_FLAGS} -DDB2INT)
set(APP_COMPILER_FLAGS_DECAY2FEEDBACK ${APP_COMPILER_FLAGS} -DDECAY2FEEDBACK)
set(APP_COMPILER_FLAGS_CALCULATE_DAMPING ${APP_COMPILER_FLAGS} -DCALCULATE_DAMPING)
set(APP_COMPILER_FLAGS_WET_DRY_MIX ${APP_COMPILER_FLAGS} -DWET_DRY_MIX)
set(APP_COMPILER_FLAGS_WET_DRY_MIX_ST ${APP_COMPILER_FLAGS} -DWET_DRY_MIX_ST)
set(APP_COMPILER_FLAGS_CUTOFF ${APP_COMPILER_FLAGS} -DCUTOFF)
unset(APP_COMPILER_FLAGS)
XMOS_REGISTER_APP()

View File

View File

@@ -0,0 +1,71 @@
// 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/reverb.h"
#include "control/reverb_plate.h"
FILE * _fopen(char * fname, char* mode) {
FILE * fp = fopen(fname, mode);
if (fp == NULL)
{
printf("Error opening a file %s\n", fname);
exit(1);
}
return fp;
}
int main(int argc, char* argv[])
{
int n_inputs = 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 = 0;
fread(&samp, sizeof(float), 1, in);
#if defined(FLOAT2INT)
int32_t ival = adsp_reverb_float2int(samp);
fwrite(&ival, sizeof(int32_t), 1, out);
#elif defined(DB2INT)
int32_t ival = adsp_reverb_db2int(samp);
fwrite(&ival, sizeof(int32_t), 1, out);
#elif defined(DECAY2FEEDBACK)
int32_t ival = adsp_reverb_calculate_feedback(samp);
fwrite(&ival, sizeof(int32_t), 1, out);
#elif defined(CALCULATE_DAMPING)
int32_t ival = adsp_reverb_calculate_damping(samp);
fwrite(&ival, sizeof(int32_t), 1, out);
#elif defined(WET_DRY_MIX)
int32_t gains[2];
adsp_reverb_wet_dry_mix(gains, samp);
fwrite(gains, sizeof(int32_t), 2, out);
#elif defined(WET_DRY_MIX_ST)
int32_t gains[3];
adsp_reverb_st_wet_dry_mix(gains, samp, 1.0);
fwrite(gains, sizeof(int32_t), 3, out);
#elif defined(CUTOFF)
int32_t ival = adsp_reverb_plate_calc_bandwidth(samp, 48000.0f);
fwrite(&ival, sizeof(int32_t), 1, out);
#else
#error "config not defined"
#endif
//printf("returned: %ld\n", ival);
//fwrite(&ival, 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/adsp_control.h"
#define FS 48000
#define MAX_ROOM 1.0
#define PD_MS 1
#define PD_SAMPS (uint32_t)(PD_MS * FS / 1000)
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()
{
float const fs = FS;
FILE *in = _fopen("../sig_2ch_48k.bin", "rb");
FILE *out = _fopen("rv_sig_out.bin", "wb");
FILE *info = _fopen("rv_info.bin", "rb");
fseek(in, 0, SEEK_END);
int in_len = (ftell(in) / sizeof(int32_t)) / 2; // stereo
fseek(in, 0, SEEK_SET);
int32_t pregain, wet1, wet2, dry, decay, dc_diff1, damp, dc_diff2, bw, in_diff1, in_diff2;
fread(&pregain, sizeof(int32_t), 1, info);
fread(&wet1, sizeof(int32_t), 1, info);
fread(&wet2, sizeof(int32_t), 1, info);
fread(&dry, sizeof(int32_t), 1, info);
fread(&decay, sizeof(int32_t), 1, info);
fread(&dc_diff2, sizeof(int32_t), 1, info);
fread(&damp, sizeof(int32_t), 1, info);
fread(&dc_diff1, sizeof(int32_t), 1, info);
fread(&bw, sizeof(int32_t), 1, info);
fread(&in_diff1, sizeof(int32_t), 1, info);
fread(&in_diff2, sizeof(int32_t), 1, info);
fclose(info);
uint8_t reverb_heap[ADSP_RVP_HEAP_SZ(FS, PD_SAMPS)] = {0};
reverb_plate_t rv;
rv.pre_gain = pregain;
rv.wet_gain1 = wet1;
rv.wet_gain2 = wet2;
rv.dry_gain = dry;
rv.decay = decay;
rv.lowpasses[0] = lowpass_1ord_init(bw);
rv.lowpasses[1] = lowpass_1ord_init(damp);
rv.lowpasses[2] = lowpass_1ord_init(damp);
adsp_reverb_plate_init_filters(&rv, fs, dc_diff1, dc_diff2, in_diff1, in_diff2, PD_SAMPS, PD_SAMPS, reverb_heap);
for (int i = 0; i < in_len; i++)
{
int32_t samp_l = 0, samp_r = 0, samp_out[2] = {0};
fread(&samp_l, sizeof(int32_t), 1, in);
fread(&samp_r, sizeof(int32_t), 1, in);
adsp_reverb_plate(&rv, samp_out, samp_l, samp_r);
fwrite(samp_out, sizeof(int32_t), 2, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,69 @@
// 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"
#define FS 48000
#define MAX_ROOM 1.0
#define PD_MS 10
#define PD_SAMPS (uint32_t)(PD_MS * FS / 1000)
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()
{
float const fs = FS;
float const max_room_size = MAX_ROOM;
float const room_size = 1.0;
FILE *in = _fopen("../rv_sig_48k.bin", "rb");
FILE *out = _fopen("rv_sig_out.bin", "wb");
FILE *info = _fopen("rv_info.bin", "rb");
fseek(in, 0, SEEK_END);
int in_len = ftell(in) / sizeof(int32_t);
fseek(in, 0, SEEK_SET);
int32_t pregain, wet, dry, feedback, damping;
fread(&pregain, sizeof(int32_t), 1, info);
fread(&wet, sizeof(int32_t), 1, info);
fread(&dry, sizeof(int32_t), 1, info);
fread(&feedback, sizeof(int32_t), 1, info);
fread(&damping, sizeof(int32_t), 1, info);
fclose(info);
uint8_t reverb_heap[ADSP_RVR_HEAP_SZ(FS, MAX_ROOM, PD_SAMPS)] = {0};
reverb_room_t rv;
rv.pre_gain = pregain;
rv.wet_gain = wet;
rv.dry_gain = dry;
adsp_reverb_room_init_filters(&rv, fs, max_room_size, PD_SAMPS, PD_SAMPS, feedback, damping, reverb_heap);
adsp_reverb_room_set_room_size(&rv, room_size);
for (int i = 0; i < in_len; i++)
{
int32_t samp = 0, samp_out = 0;
fread(&samp, sizeof(int32_t), 1, in);
samp_out = adsp_reverb_room(&rv, samp);
fwrite(&samp_out, sizeof(int32_t), 1, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,72 @@
// 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"
#define FS 48000
#define MAX_ROOM 1.0
#define PD_MS 1
#define PD_SAMPS (uint32_t)(PD_MS * FS / 1000)
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()
{
float const fs = FS;
float const max_room_size = MAX_ROOM;
float const room_size = 1.0;
FILE *in = _fopen("../sig_2ch_48k.bin", "rb");
FILE *out = _fopen("rv_sig_out.bin", "wb");
FILE *info = _fopen("rv_info.bin", "rb");
fseek(in, 0, SEEK_END);
int in_len = (ftell(in) / sizeof(int32_t)) / 2; // stereo
fseek(in, 0, SEEK_SET);
int32_t pregain, wet1, wet2, dry, feedback, damping;
fread(&pregain, sizeof(int32_t), 1, info);
fread(&wet1, sizeof(int32_t), 1, info);
fread(&wet2, sizeof(int32_t), 1, info);
fread(&dry, sizeof(int32_t), 1, info);
fread(&feedback, sizeof(int32_t), 1, info);
fread(&damping, sizeof(int32_t), 1, info);
fclose(info);
uint8_t reverb_heap[ADSP_RVRST_HEAP_SZ(FS, MAX_ROOM, PD_SAMPS)] = {0};
reverb_room_st_t rv;
rv.pre_gain = pregain;
rv.wet_gain1 = wet1;
rv.wet_gain2 = wet2;
rv.dry_gain = dry;
adsp_reverb_room_st_init_filters(&rv, fs, max_room_size, PD_SAMPS, PD_SAMPS, feedback, damping, reverb_heap);
adsp_reverb_room_st_set_room_size(&rv, room_size);
for (int i = 0; i < in_len; i++)
{
int32_t samp_l = 0, samp_r = 0, samp_out[2] = {0};
fread(&samp_l, sizeof(int32_t), 1, in);
fread(&samp_r, sizeof(int32_t), 1, in);
adsp_reverb_room_st(&rv, samp_out, samp_l, samp_r);
fwrite(samp_out, sizeof(int32_t), 2, out);
}
fclose(in);
fclose(out);
return 0;
}

View File

@@ -0,0 +1,99 @@
# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
from audio_dsp.dsp.generic import Q_SIG
import audio_dsp.dsp.reverb as reverb
import audio_dsp.dsp.signal_gen as gen
import numpy as np
from pathlib import Path
import pytest
import shutil
import soundfile as sf
import subprocess
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 = "rv_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_32")
return sig_fl
def get_c_wav(dir_name, app_name, verbose=False, sim=True):
app = "xsim" if sim else "xrun --io"
run_cmd = app + " " + str(BIN_DIR / app_name)
stdout = subprocess.check_output(run_cmd, cwd=dir_name, shell=True)
if verbose: print("run msg:\n", stdout.decode())
sig_bin = dir_name / "rv_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_32")
return sig_fl
def run_py(uut: reverb.reverb_room, sig_fl, use_float_sig=True):
out_int = np.zeros(sig_fl.size)
sig_int = float_to_qxx(sig_fl)
if use_float_sig:
for n in range(sig_fl.size):
out_int[n] = uut.process_xcore(sig_fl[n])
else:
for n in range(sig_fl.size):
out_int[n] = uut.process_xcore(sig_int[n])
out_int = qxx_to_float(out_int)
# sf.write(GEN_DIR / "sig_py_int.wav", out_int, FS, "PCM_32")
return out_int
@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("decay, damping", [[1.0, 1.0],
[0.1, 0.5]])
@pytest.mark.parametrize("wet, dry, pregain", [[-1.0, -1.0, 0.015]])
def test_reverb_room(in_signal, decay, damping, wet, dry, pregain):
n_chans = 1
fs = FS
max_room_size = 1.0
room_size = 1.0
predelay = 10
uut = reverb.reverb_room(fs, n_chans, max_room_size, room_size, decay, damping, wet, dry, pregain, predelay)
test_name = f"reverb_room_{decay}_{damping}_{wet}_{dry}_{pregain}"
test_dir = BIN_DIR / test_name
test_dir.mkdir(exist_ok = True, parents = True)
rv_info = [uut.pregain_int, uut.wet_int, uut.dry_int, uut.combs[0].feedback_int, uut.combs[0].damp1_int]
rv_info = np.array(rv_info, dtype=np.int32)
rv_info.tofile(test_dir / "rv_info.bin")
out_py_int = run_py(uut, in_signal)
out_c = get_c_wav(test_dir, "reverb_test.xe")
shutil.rmtree(test_dir)
np.testing.assert_allclose(out_c, out_py_int, rtol=0, atol=0)

View File

@@ -0,0 +1,165 @@
# 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
from audio_dsp.dsp.reverb_base import Q_VERB
from audio_dsp.dsp.reverb import reverb_room
from audio_dsp.dsp.reverb_stereo import reverb_room_stereo
from audio_dsp.dsp.reverb_plate import reverb_plate_stereo
from subprocess import run
from pathlib import Path
import os
CWD = Path(__file__).parent
TOL_KWARGS = dict(rtol=2**-16, atol=0)
LESS_THAN_1 = ((2**Q_VERB) - 1) / (2**Q_VERB)
def q_verb(x):
return int(x * 2**Q_VERB)
def db2lin(db):
return 10 ** (db / 20)
def new_reverb(**kwargs):
return reverb_room(48000, 1, **kwargs)
def get_c(config, val):
bin_dir = CWD / "bin" / config
out_dir = CWD / "bin" / f"{config}_{val}"
os.makedirs(out_dir, exist_ok=True)
sig_fl32 = np.array(val).astype(np.float32)
name = "test_vector"
sig_fl32.tofile(out_dir / f"{name}.bin")
xe = bin_dir / f"reverb_converters_{config}.xe"
run(["xsim", str(xe)], check=True, cwd=out_dir)
#print(out_dir)
return np.fromfile(out_dir / "out_vector.bin", dtype=np.int32)
def db2int(db):
return q_verb(db2lin(db))
def get_output(config, input, sattr, gattr):
c_val = get_c(config, input)[0]
r = new_reverb()
setattr(r, sattr, input)
p_val = getattr(r, gattr)
return c_val, p_val
@pytest.mark.parametrize(
"sattr,gattr",
[
["wet_db", "wet_int"],
["dry_db", "dry_int"],
],
)
@pytest.mark.parametrize(
"input,expected",
[
[-6, db2int(-6)],
[0, q_verb(1)],
[1, q_verb(1)],
[6, q_verb(1)],
],
)
def test_reverb_db2int(sattr, gattr, input, expected):
np.testing.assert_allclose(
get_output("DB2INT", input, sattr, gattr), expected, **TOL_KWARGS
)
@pytest.mark.parametrize(
"input,expected",
[
[0, q_verb(0.7)],
[-1, q_verb(0.7)],
[1, q_verb(0.98)],
[1.1, q_verb(0.98)],
],
)
def test_reverb_decay2feedback(input, expected):
np.testing.assert_allclose(
get_output("DECAY2FEEDBACK", input, "decay", "feedback_int"),
expected,
**TOL_KWARGS,
)
@pytest.mark.parametrize(
"input,expected",
[
[-0.5, 0],
[0, 0],
[0.5, q_verb(0.5)],
[LESS_THAN_1, q_verb(LESS_THAN_1)],
[1, q_verb(LESS_THAN_1)],
[2, q_verb(LESS_THAN_1)],
],
)
def test_reverb_float2int(input, expected):
np.testing.assert_allclose(
get_output("FLOAT2INT", input, "pregain", "pregain_int"), expected, **TOL_KWARGS
)
@pytest.mark.parametrize(
"input,expected",
[
[-0.5, 1],
[0, 1],
[0.5, q_verb(0.5)],
[LESS_THAN_1, q_verb(LESS_THAN_1)],
[1, q_verb(LESS_THAN_1)],
[2, q_verb(LESS_THAN_1)],
],
)
def test_reverb_damping(input, expected):
np.testing.assert_allclose(
get_output("CALCULATE_DAMPING", input, "damping", "damping_int"),
expected,
**TOL_KWARGS,
)
@pytest.mark.parametrize(
"input", [-1, 0, 0.07, 0.23, 0.5, 0.64, 0.92, 1, 2]
)
def test_reverb_wet_dry_mix_conv(input):
c_vals = get_c("WET_DRY_MIX", input)
r = new_reverb()
r.set_wet_dry_mix(input)
p_vals = np.array([r.dry_int, r.wet_int], dtype=np.int32)
# -23 cause of the float to int conversion
np.testing.assert_allclose(c_vals, p_vals, rtol=2**-23, atol=0)
@pytest.mark.parametrize(
"input", [-1, 0, 0.07, 0.23, 0.5, 0.64, 0.92, 1, 2]
)
def test_reverb_st_wet_dry_mix_conv(input):
c_vals = get_c("WET_DRY_MIX_ST", input)
r = reverb_room_stereo(48000, 2)
r.set_wet_dry_mix(input)
p_vals = np.array([r.dry_int, r.wet_1_int, r.wet_2_int], dtype=np.int32)
# -23 cause of the float to int conversion
np.testing.assert_allclose(c_vals, p_vals, rtol=2**-23, atol=0)
@pytest.mark.parametrize(
"input", [-1, 100, 1000, 2000, 4000, 8000, 16000, 32000]
)
def test_reverb_cutoff(input):
c_vals = get_c("CUTOFF", input)
r = reverb_plate_stereo(48000, 2)
r.bandwidth = input
p_vals = r.lowpasses[0].coeff_b0_int
# -23 cause of the float to int conversion
np.testing.assert_allclose(c_vals, p_vals, rtol=2**-13, atol=0)
if __name__ == "__main__":
test_reverb_cutoff(8000)

View File

@@ -0,0 +1,510 @@
# 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 functools import partial
import warnings
import audio_dsp.dsp.utils as utils
import audio_dsp.dsp.signal_gen as gen
import audio_dsp.dsp.generic as dspg
import audio_dsp.dsp.reverb as rv
import audio_dsp.dsp.reverb_stereo as rvs
import audio_dsp.dsp.reverb_plate as rvp
@pytest.mark.parametrize("signal, freq", [["sine", 20],
["sine", 1000],
["sine", 10000],
["sine", 23000],
["noise", None]])
@pytest.mark.parametrize("algo, param", [["mono_room", 0.1],
["mono_room", 1],
["mono_room", 4],
["stereo_room", 0.1],
["stereo_room", 1],
["stereo_room", 4],
["stereo_plate", 0.95],]
)
def test_reverb_overflow(signal, freq, algo, param):
# check no overflow errors occur
fs = 48000
q_format = 31
if signal == "sine":
sig = gen.sin(fs, 5, freq, 1)
elif signal == "chirp":
sig = gen.log_chirp(fs, 5, 1, 20, 20000)
elif signal == "noise":
sig = gen.white_noise(fs, 5, 1)
sig = sig/np.max(np.abs(sig))
if algo == "stereo_room":
sig = np.tile(sig, [2, 1])
reverb = rvs.reverb_room_stereo(fs, 2, max_room_size=param, room_size=1, decay=1.0, damping=0.0, Q_sig=q_format)
elif algo == "mono_room":
reverb = rv.reverb_room(fs, 1, max_room_size=param, room_size=1, decay=1.0, damping=0.0, Q_sig=q_format)
elif algo == "stereo_plate":
sig = np.tile(sig, [2, 1])
reverb = rvp.reverb_plate_stereo(fs, 2, decay=param, damping=0.0, Q_sig=q_format)
#print(reverb.get_buffer_lens())
output_xcore = np.zeros_like(sig)
output_flt = np.zeros_like(sig)
if "stereo" in algo:
for n in range(sig.shape[1]):
output_xcore[:, n] = reverb.process_channels_xcore(sig[:, n])
reverb.reset_state()
for n in range(sig.shape[1]):
output_flt[:, n] = reverb.process_channels(sig[:, n])
else:
for n in range(len(sig)):
output_xcore[n] = reverb.process_xcore(sig[n])
reverb.reset_state()
for n in range(len(sig)):
output_flt[n] = reverb.process(sig[n])
def calc_reverb_time(in_sig, reverb_output):
# extend by 2x
sig = np.concatenate((in_sig, np.zeros_like(in_sig)))
output_xcore = np.concatenate((reverb_output, np.zeros_like(reverb_output)))
sig_spect = np.fft.rfft(sig)
output_xcore_spect = np.fft.rfft(output_xcore)
# Y = HX, Y/X = H
H_xcore_spect = output_xcore_spect/sig_spect
h_xcore = np.fft.irfft(H_xcore_spect)
h_xcore = h_xcore[:len(h_xcore)//2]
return h_xcore
@pytest.mark.parametrize("max_room_size_diffusion", [0.5, 0.9])
@pytest.mark.parametrize("decay, damping", [[0.5, 0.35],
[1.0, 0.0]])
@pytest.mark.parametrize("q_format", [27, 31])
@pytest.mark.parametrize("algo, width", [["mono_room", None],
["stereo_room", 1.0],
["stereo_plate", 1.0],]
)
@pytest.mark.parametrize("wdmix", [0.5, 1.0])
def test_reverb_time(max_room_size_diffusion, decay, damping, q_format, width, algo, wdmix):
# measure reverb time with chirp
fs = 48000
if "plate" in algo:
pregain = 0.5**(q_format - 26)
else:
pregain = 0.015 * 2**(27 - q_format)
sig = np.zeros(int(fs*max_room_size_diffusion*6) + fs)
sig[:1*fs] = gen.log_chirp(fs, 1, 1, 20, 20000)
sig = sig* (2**q_format - 1)/(2**q_format)
if algo =="stereo_room":
sig = np.tile(sig, [2, 1])
reverb = rvs.reverb_room_stereo(fs, 2, max_room_size=max_room_size_diffusion, room_size=1, decay=decay, damping=damping, Q_sig=q_format, pregain=pregain, width=width)
elif algo =="mono_room":
reverb = rv.reverb_room(fs, 1, max_room_size=max_room_size_diffusion, room_size=1, decay=decay, damping=damping, Q_sig=q_format, pregain=pregain)
elif algo =="stereo_plate":
sig = np.tile(sig, [2, 1])
reverb = rvp.reverb_plate_stereo(fs, 2, early_diffusion=max_room_size_diffusion,
late_diffusion=max_room_size_diffusion,
decay=decay, damping=damping, Q_sig=q_format,
pregain=pregain, width=width)
reverb.set_wet_dry_mix(wdmix)
output_xcore = np.zeros_like(sig)
output_flt = np.zeros_like(sig)
if "stereo" in algo:
for n in range(sig.shape[1]):
output_flt[:, n] = reverb.process_channels(sig[:, n])
reverb.reset_state()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always', utils.SaturationWarning)
for n in range(sig.shape[1]):
output_xcore[:, n] = reverb.process_channels_xcore(sig[:, n])
else:
for n in range(len(sig)):
output_flt[n] = reverb.process(sig[n])
reverb.reset_state()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always', utils.SaturationWarning)
for n in range(len(sig)):
output_xcore[n] = reverb.process_xcore(sig[n])
# if we triggered a saturation warning, can't guarantee arrays are the same
sat_warn_flag = any([wi.category is utils.SaturationWarning for wi in w])
# # in this case, pregain should be adjusted
# if sat_warn_flag: assert False
# small signals are always going to be ropey due to quantizing, so just check average error of top half
top_half = np.logical_and(utils.db(output_flt) > -50, utils.db(output_flt) < (6*(31-q_format)))
if np.any(top_half):
error_flt = np.abs(utils.db(output_xcore[top_half])-utils.db(output_flt[top_half]))
mean_error_flt = utils.db(np.nanmean(utils.db2gain(error_flt)))
assert mean_error_flt < 0.055
@pytest.mark.parametrize("max_room_size", [0.5])
@pytest.mark.parametrize("decay", [0.5])
@pytest.mark.parametrize("damping", [0.5])
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_noise_floor(max_room_size, decay, damping, algo):
# check the reverb decays to 0 (no limit cycle noise)
fs = 48000
q_format = 27
sig = np.zeros(int(fs*max_room_size*40) + fs)
sig[:1*fs] = gen.log_chirp(fs, 1, 1, 20, 20000)
sig = sig* (2**q_format - 1)/(2**q_format)
if algo =="stereo_room":
sig = np.tile(sig, [2, 1])
reverb = rvs.reverb_room_stereo(fs, 2, max_room_size=max_room_size, room_size=1, decay=decay, damping=damping, Q_sig=q_format)
elif algo =="mono_room":
reverb = rv.reverb_room(fs, 1, max_room_size=max_room_size, room_size=1, decay=decay, damping=damping, Q_sig=q_format)
elif algo == "stereo_plate":
sig = np.tile(sig, [2, 1])
reverb = rvp.reverb_plate_stereo(fs, 2, decay=decay, damping=damping, Q_sig=q_format)
#print(reverb.get_buffer_lens())
output_xcore = np.zeros_like(sig)
output_flt = np.zeros_like(sig)
if "stereo" in algo:
for n in range(sig.shape[1]):
output_flt[:, n] = reverb.process_channels(sig[:, n])
reverb.reset_state()
with warnings.catch_warnings(record=True) as w:
for n in range(sig.shape[1]):
output_xcore[:, n] = reverb.process_channels_xcore(sig[:, n])
else:
for n in range(len(sig)):
output_flt[n] = reverb.process(sig[n])
reverb.reset_state()
with warnings.catch_warnings(record=True) as w:
for n in range(len(sig)):
output_xcore[n] = reverb.process_xcore(sig[n])
# check noise floor
if "stereo" in algo:
assert np.max(np.abs(output_xcore[:, -1000:])) < 2**-(reverb.Q_sig + 1)
else:
assert np.max(np.abs(output_xcore[-1000:])) < 2**-(reverb.Q_sig + 1)
# small signals are always going to be ropey due to quantizing, so just check average error of top half
top_half = np.logical_and(utils.db(output_flt) > -50, utils.db(output_flt) < (6*(31-q_format)))
if np.any(top_half):
error_flt = np.abs(utils.db(output_xcore[top_half])-utils.db(output_flt[top_half]))
mean_error_flt = utils.db(np.nanmean(utils.db2gain(error_flt)))
assert mean_error_flt < 0.055
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_bypass(algo):
# test that a drc component is bit exact when the signal is below
# the threshold (or above in the case of a noise gate).
fs = 48000
signal = gen.log_chirp(fs, 0.5, 1)
if algo == "stereo_room":
signal = np.tile(signal, [2, 1])
reverb = rvs.reverb_room_stereo(fs, 2, dry_gain_db=0, wet_gain_db=-np.inf)
elif algo == "mono_room":
reverb = rv.reverb_room(fs, 1, dry_gain_db=0, wet_gain_db=-np.inf)
elif algo == "stereo_plate":
signal = np.tile(signal, [2, 1])
reverb = rvp.reverb_plate_stereo(fs, 2, dry_gain_db=0, wet_gain_db=-np.inf)
output_xcore = np.zeros_like(signal)
output_flt = np.zeros_like(signal)
if "stereo" in algo:
for n in range(signal.shape[1]):
output_xcore[:, n] = reverb.process_channels_xcore(signal[:, n])
reverb.reset_state()
for n in range(signal.shape[1]):
output_flt[:, n] = reverb.process_channels(signal[:, n])
else:
for n in range(len(signal)):
output_xcore[n] = reverb.process_xcore(signal[n])
reverb.reset_state()
for n in range(len(signal)):
output_flt[n] = reverb.process(signal[n])
np.testing.assert_array_equal(signal, output_flt)
# quantization noise from multiply by dry gain
np.testing.assert_allclose(signal, output_xcore, atol=2**-(reverb.Q_sig-1))
@pytest.mark.parametrize("algo", ["stereo_room", "stereo_plate"])
@pytest.mark.parametrize("width", [0, 1])
def test_reverb_width(algo, width):
# test that a drc component is bit exact when the signal is below
# the threshold (or above in the case of a noise gate).
fs = 48000
signal = gen.log_chirp(fs, 0.5, 1)
if algo == "stereo_room":
signal = np.tile(signal, [2, 1])
reverb = rvs.reverb_room_stereo(fs, 2)
elif algo == "mono_room":
reverb = rv.reverb_room(fs, 1)
elif algo == "stereo_plate":
signal = np.tile(signal, [2, 1])
reverb = rvp.reverb_plate_stereo(fs, 2)
reverb.width = width
output_xcore = np.zeros_like(signal)
output_flt = np.zeros_like(signal)
if "stereo" in algo:
for n in range(signal.shape[1]):
output_xcore[:, n] = reverb.process_channels_xcore(signal[:, n])
reverb.reset_state()
for n in range(signal.shape[1]):
output_flt[:, n] = reverb.process_channels(signal[:, n])
else:
for n in range(len(signal)):
output_xcore[n] = reverb.process_xcore(signal[n])
reverb.reset_state()
for n in range(len(signal)):
output_flt[n] = reverb.process(signal[n])
# small signals are always going to be ropey due to quantizing, so just check average error of top half
q_format = 27
top_half = np.logical_and(utils.db(output_flt) > -50, utils.db(output_flt) < (6*(31-q_format)))
if np.any(top_half):
error_flt = np.abs(utils.db(output_xcore[top_half])-utils.db(output_flt[top_half]))
mean_error_flt = utils.db(np.nanmean(utils.db2gain(error_flt)))
assert mean_error_flt < 0.055
if width == 0:
assert np.all(output_xcore[0, :] == output_xcore)
assert np.all(output_flt[0, :] == output_flt)
else:
assert not np.all(output_xcore[0, :] == output_xcore[1:, :])
assert not np.all(output_flt[0, :] == output_flt[1:, :])
@pytest.mark.parametrize("fs", [48000])
@pytest.mark.parametrize("q_format", [27, 31])
@pytest.mark.parametrize("algo, param", [["mono_room", 0.1],
["mono_room", 1],
["mono_room", 4],
["stereo_room", 0.1],
["stereo_room", 1],
["stereo_room", 4],
["stereo_plate", 0.1],
["stereo_plate", 0.5],
["stereo_plate", 0.9],]
)
def test_reverb_frames(fs, q_format, algo, param):
# test the process_frame functions of the reverb components
# if q_format > 27:
# pytest.xfail("This test is not meant to pass with a q more then 27")
if algo == "stereo_room":
reverb = rvs.reverb_room_stereo(fs, 2, max_room_size=param, width=0, Q_sig=q_format)
signal = gen.log_chirp(fs, 0.5, 1)
t = np.arange(len(signal))/fs
signal *= np.sin(t*2*np.pi*0.5)
signal = np.tile(signal, [2, 1])
elif algo == "mono_room":
reverb = rv.reverb_room(fs, 1, max_room_size=param, Q_sig=q_format)
signal = gen.log_chirp(fs, 0.5, 1)
t = np.arange(len(signal))/fs
signal *= np.sin(t*2*np.pi*0.5)
signal = np.tile(signal, [1, 1])
elif algo == "stereo_plate":
reverb = rvp.reverb_plate_stereo(fs, 2, decay=param, width=0, Q_sig=q_format)
signal = gen.log_chirp(fs, 0.5, 1)
t = np.arange(len(signal))/fs
signal *= np.sin(t*2*np.pi*0.5)
signal = np.tile(signal, [2, 1])
frame_size = 1
signal_frames = utils.frame_signal(signal, frame_size, 1)
output_int = np.zeros_like(signal)
output_flt = np.zeros_like(signal)
for n in range(len(signal_frames)):
output_int[:, n*frame_size:(n+1)*frame_size] = reverb.process_frame_xcore(signal_frames[n])
reverb.reset_state()
for n in range(len(signal_frames)):
output_flt[:, n*frame_size:(n+1)*frame_size] = reverb.process_frame(signal_frames[n])
assert np.all(output_int[0, :] == output_int)
assert np.all(output_flt[0, :] == output_flt)
@pytest.mark.parametrize("ratio", [0, 0.5, 1])
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_wet_dry_mix(ratio, algo):
fs = 48000
q_format = 27
max_room_sz = 1
room_sz = 1
damp = 0.22
a = utils.db2gain(-10)
sig = gen.pink_noise(fs, 1, a)
if algo == "stereo_room":
sig = np.tile(sig, [2, 1])
verb = rvs.reverb_room_stereo(fs, 2, max_room_size=max_room_sz, damping=damp, room_size=room_sz, Q_sig=q_format)
elif algo =="mono_room":
verb = rv.reverb_room(fs, 1, max_room_size=max_room_sz, damping=damp, room_size=room_sz, Q_sig=q_format)
elif algo == "stereo_plate":
sig = np.tile(sig, [2, 1])
verb = rvp.reverb_plate_stereo(fs, 2, Q_sig=q_format)
verb.set_wet_dry_mix(ratio)
sig_py = np.zeros_like(sig)
sig_xc = np.zeros_like(sig)
if "stereo" in algo:
for i in range(sig.shape[1]):
sig_py[:, i] = verb.process_channels(sig[:, i])
verb.reset_state()
for i in range(sig.shape[1]):
sig_xc[:, i] = verb.process_channels_xcore(sig[:, i])
else:
for i in range(len(sig)):
sig_py[i] = verb.process(sig[i])
verb.reset_state()
for i in range(len(sig)):
sig_xc[i] = verb.process_xcore(sig[i])
# small signals are always going to be ropey due to quantizing, so just check average error of top half
top_half = utils.db(sig_py) > -50
if np.any(top_half):
error_vpu = np.abs(utils.db(sig_py[top_half])-utils.db(sig_xc[top_half]))
mean_error_vpu = utils.db(np.nanmean(utils.db2gain(error_vpu)))
assert mean_error_vpu < 0.005
def get_algo_partial(algo):
if algo =="stereo_room":
r = partial(rvs.reverb_room_stereo, 48000, 2)
elif algo =="mono_room":
r = partial(rv.reverb_room, 48000, 1)
elif algo =="stereo_plate":
r = partial(rvp.reverb_plate_stereo, 48000, 2)
return r
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_properties_decay(algo):
"""Basic tests to check for consistency when setting the properties."""
r = get_algo_partial(algo)
val = 0.1
a = r(decay=val)
b = r()
b.decay = val
should_be_val = np.array([i.decay for i in (a, b)])
np.testing.assert_allclose(should_be_val, val)
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_properties_pregain(algo):
"""Basic tests to check for consistency when setting the properties."""
r = get_algo_partial(algo)
val = 0.1
a = r(pregain=val)
b = r()
b.pregain = val
should_be_val = np.array([i.pregain for i in (a, b)])
np.testing.assert_allclose(should_be_val, val)
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_properties_wet_db(algo):
"""Basic tests to check for consistency when setting the properties."""
r = get_algo_partial(algo)
val = -6
a = r(wet_gain_db=val)
b = r()
b.wet_db = val
should_be_val = np.array([i.wet_db for i in (a, b)])
np.testing.assert_allclose(should_be_val, val)
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_properties_dry_db(algo):
"""Basic tests to check for consistency when setting the properties."""
r = get_algo_partial(algo)
val = -6
a = r(dry_gain_db=val)
b = r()
b.dry_db = val
should_be_val = np.array([i.dry_db for i in (a, b)])
np.testing.assert_allclose(should_be_val, val)
@pytest.mark.parametrize("algo", ["mono_room", "stereo_room", "stereo_plate"])
def test_reverb_properties_damping(algo):
"""Basic tests to check for consistency when setting the properties."""
r = get_algo_partial(algo)
val = 0.5
a = r(damping=val)
b = r()
b.damping = val
should_be_val = np.array([i.damping for i in (a, b)])
np.testing.assert_allclose(should_be_val, val)
@pytest.mark.parametrize("stereo", [True, False])
def test_reverb_properties_room_size(stereo):
"""Basic tests to check for consistency when setting the properties."""
if stereo:
r = partial(rvs.reverb_room_stereo, 48000, 2)
else:
r = partial(rv.reverb_room, 48000, 1)
val = 0.5
a = r(room_size=val)
b = r()
b.room_size = val
should_be_val = np.array([i.room_size for i in (a, b)])
np.testing.assert_allclose(should_be_val, val)
if __name__ == "__main__":
test_reverb_width("stereo_plate", 1)
# test_reverb_time(0.5, 0.25, 0.35, 29, 0.5, 1, "stereo_plate")
# test_reverb_overflow("sine", 20, "stereo_plate", 0.1)
# test_reverb_time(0.01, 1)
# test_reverb_frames(48000, 27, "stereo_plate", 0.5)
# test_reverb_wet_dry_mix(1.0, "stereo_plate")
# test_reverb_bypass_stereo()
# test_reverb_noise_floor_stereo(1.0, 1.0, 0)
# test_reverb_time(0.01, 1, 0, 31, 0.001, 0.5)
# test_reverb_properties_room_size(True)

View File

@@ -0,0 +1,126 @@
# 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 audio_dsp.dsp.reverb_stereo as rvs
import audio_dsp.dsp.reverb_plate as rvp
import audio_dsp.dsp.signal_gen as gen
import audio_dsp.dsp.utils as utils
import audio_dsp.dsp.generic as dspg
import pytest
import subprocess
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_2ch(len=0.05):
sig_l = []
sig_l.append(gen.sin(fs, len, 997, 0.7))
sig_l.append(gen.log_chirp(fs, len, 0.5))
sig_fl_t = np.stack(sig_l, axis=1)
sig_fl_t = utils.saturate_float_array(sig_fl_t, dspg.Q_SIG)
sig_fl_t = q_convert_flt(sig_fl_t, 23, dspg.Q_SIG)
sig_int = float_to_qxx(sig_fl_t)
sig_path = bin_dir / "sig_2ch_48k.bin"
xdist_safe_bin_write(sig_int, sig_path)
sf.write(gen_dir / "sig_2ch_48k.wav", sig_fl_t, int(fs), "PCM_24")
return sig_fl_t.T
def get_c_wav(dir_name, app_name, verbose=False, sim=True):
app = "xsim" if sim else "xrun --io"
run_cmd = app + " " + str(bin_dir / app_name)
stdout = subprocess.check_output(run_cmd, cwd=dir_name, shell=True)
if verbose: print("run msg:\n", stdout.decode())
sig_bin = dir_name / "rv_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)
#deinterleave channels
sig_int0 = sig_int[0::2]
sig_int1 = sig_int[1::2]
sig_int = [sig_int0, sig_int1]
sig_int = np.stack(sig_int, axis=0)
sig_fl = qxx_to_float(sig_int)
sf.write(gen_dir / "sig_c.wav", sig_fl.T, fs, "PCM_24")
return sig_fl
def run_py(rv, sig_fl):
out_py = np.zeros_like(sig_fl)
for n in range(sig_fl.shape[1]):
out_py[:,n] = rv.process_channels_xcore(sig_fl[:,n])
sf.write(gen_dir / "sig_py_int.wav", out_py.T, fs, "PCM_24")
return out_py
@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_2ch()
@pytest.mark.parametrize("decay, damping", [[1.0, 1.0],
[0.1, 0.5]
])
@pytest.mark.parametrize("wet, dry, pregain", [[-1.0, -1.0, 0.015]])
def test_reverb_room_st_c(in_signal, decay, damping, wet, dry, pregain):
n_chans = 2
max_room_size = 1.0
room_size = 1.0
predelay = 1
width = 1.0
rv = rvs.reverb_room_stereo(fs, n_chans, max_room_size, room_size, decay, damping, width, wet, dry, pregain, predelay)
test_name = f"reverb_room_stereo_{decay}_{damping}_{wet}_{dry}_{pregain}"
rv_info = [rv.pregain_int, rv.wet_1_int, rv.wet_2_int, rv.dry_int, rv.combs_l[0].feedback_int, rv.combs_l[0].damp1_int]
rv_info = np.array(rv_info, dtype=np.int32)
test_dir = bin_dir / test_name
test_dir.mkdir(exist_ok = True, parents = True)
rv_info.tofile(test_dir / "rv_info.bin")
out_py_int = run_py(rv, in_signal)
out_c = get_c_wav(test_dir, "reverb_st_test.xe")
shutil.rmtree(test_dir)
np.testing.assert_allclose(out_c, out_py_int, rtol=0, atol=0)
@pytest.mark.parametrize("decay, damping", [[1.0, 1.0],
[0.1, 0.5]
])
@pytest.mark.parametrize("wet, dry, pregain", [[-1.0, -1.0, 0.015]])
def test_reverb_plate_c(in_signal, decay, damping, wet, dry, pregain):
n_chans = 2
predelay = 1
#width = 1.0
rv = rvp.reverb_plate_stereo(fs, n_chans, decay = decay, damping = damping, predelay = predelay, pregain = pregain, wet_gain_db = wet, dry_gain_db = dry)
test_name = f"reverb_plate_{decay}_{damping}_{wet}_{dry}_{pregain}"
# [pregain, we1, we2, dry, decay, decay_dif, damp, diffusion, bandwidth, in_dif1, in_dif2]
rv_info = [rv.pregain_int, rv.wet_1_int, rv.wet_2_int, rv.dry_int, rv.decay_int, rv.allpasses[4].feedback_int, rv.lowpasses[1].coeff_b0_int,
rv.mod_allpasses[0].feedback_int, rv.lowpasses[0].coeff_b0_int, rv.allpasses[0].feedback_int, rv.allpasses[2].feedback_int]
rv_info = np.array(rv_info, dtype=np.int32)
test_dir = bin_dir / test_name
test_dir.mkdir(exist_ok = True, parents = True)
rv_info.tofile(test_dir / "rv_info.bin")
out_py_int = run_py(rv, in_signal)
out_c = get_c_wav(test_dir, "reverb_plate_test.xe")
shutil.rmtree(test_dir)
np.testing.assert_allclose(out_c, out_py_int, rtol=0, atol=0)