Files
3d_audio/lib_audio_dsp/test/reverb/test_reverb_python.py

511 lines
19 KiB
Python
Raw Normal View History

2025-12-11 09:43:42 +08:00
# 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)