Files
3d_audio/lib_audio_dsp/python/audio_dsp/dsp/reverb.py
Steven Dan d8b2974133 init
2025-12-11 09:43:42 +08:00

532 lines
17 KiB
Python

# Copyright 2024-2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
"""DSP blocks for room reverb effects."""
from audio_dsp import _deprecated
import audio_dsp.dsp.generic as dspg
import numpy as np
import warnings
import audio_dsp.dsp.utils as utils
import audio_dsp.dsp.signal_chain as sc
from copy import deepcopy
import audio_dsp.dsp.reverb_base as rvb
import audio_dsp.dsp.filters as fltr
class allpass_fv(fltr.allpass):
"""A freeverb style all-pass filter, for use in the reverb_room
block.
This is only designed to be called inside reverb_room algorithms.
"""
def __init__(self, max_delay, feedback_gain):
# max delay cannot be changed, or you'll overflow the buffer
self._max_delay = max_delay
self._buffer = np.zeros(self._max_delay)
self._buffer_int = [0] * self._max_delay
self.delay = max_delay
self.feedback = feedback_gain
self._buffer_idx = 0
def process(self, sample): # type: ignore : overloads base class
"""
Apply an all pass filter to a signal, using floating point maths.
Take one new sample and return the filtered sample.
Input should be scaled with 0 dB = 1.0.
"""
buff_out = self._buffer[self._buffer_idx]
output = -sample + buff_out
self._buffer[self._buffer_idx] = sample + (buff_out * self.feedback)
self._buffer_idx += 1
if self._buffer_idx >= self.delay:
self._buffer_idx = 0
return output
def process_xcore(self, sample_int): # type: ignore : overloads base class
"""
Apply an all pass filter to a signal, using fixed point maths.
Take one new sample and return the filtered sample.
Input should be scaled with 0 dB = 2**Q_SIG.
Parameters
----------
sample_int : int
Input sample as an integer.
"""
assert isinstance(sample_int, int), "Input sample must be an integer"
buff_out = self._buffer_int[self._buffer_idx]
# reverb pregain should be scaled so this doesn't overflow, but
# catch it if it does
output = utils.int64(-sample_int + buff_out)
output = utils.saturate_int64_to_int32(output)
# do buffer calculation in int64 accumulator so we only quantize once
new_buff = utils.int64((sample_int << rvb.Q_VERB) + buff_out * self.feedback_int)
self._buffer_int[self._buffer_idx] = rvb.scale_sat_int64_to_int32_floor(new_buff)
# move buffer head
self._buffer_idx += 1
if self._buffer_idx >= self.delay:
self._buffer_idx = 0
return output
class comb_fv(dspg.dsp_block):
"""A freeverb style comb filter for use in the reverb_room block.
This is only designed to be called inside reverb_room algorithms.
Parameters
----------
max_delay : int
Maximum delay of the comb filter.
feedback_gain : float
Gain applied to the delayed feedback path in the comb filter, sets
the reverb time.
damping : float
Sets the low pass feedback coefficient.
"""
def __init__(self, max_delay, feedback_gain, damping):
# max delay cannot be changed, or you'll overflow the buffer
self._max_delay = max_delay
self._buffer = np.zeros(self._max_delay)
self._buffer_int = [0] * self._max_delay
self.delay = 0
self.set_feedback(feedback_gain)
self._buffer_idx = 0
self._filterstore = 0.0
self._filterstore_int = 0
self.set_damping(damping)
def set_delay(self, delay):
"""Set the length of the delay line. Will saturate to max_delay."""
if delay <= self._max_delay:
self.delay = delay
else:
self.delay = self._max_delay
warnings.warn(
"Delay cannot be greater than max delay, setting to max delay", UserWarning
)
def set_feedback(self, feedback):
"""Set the feedback of the comb filter, which controls the
reverberation time.
"""
self.feedback = feedback
self.feedback_int = rvb.float_to_q_verb(self.feedback)
def set_damping(self, damping):
"""Set the damping of the reverb, which controls how much high
frequency damping is in the room. Higher damping will give
shorter reverberation times at high frequencies.
"""
self.damp1 = damping
self.damp2 = 1 - self.damp1
# super critical these add up, but also don't overflow int32...
self.damp1_int = max(utils.int32(self.damp1 * 2**rvb.Q_VERB - 1), 1)
self.damp2_int = utils.int32((2**31 - 1) - self.damp1_int + 1)
def reset_state(self):
"""Reset all the delay line and filterstore values to zero."""
self._buffer = np.zeros(self._max_delay)
self._buffer_int = [0] * self._max_delay
self._filterstore = 0.0
self._filterstore_int = 0
def process(self, sample): # type: ignore : overloads base class
"""
Apply a comb filter to a signal, using floating point maths.
Take one new sample and return the filtered sample.
Input should be scaled with 0 dB = 1.0.
"""
output = self._buffer[self._buffer_idx]
self._filterstore = (output * self.damp2) + (self._filterstore * self.damp1)
self._buffer[self._buffer_idx] = sample + (self._filterstore * self.feedback)
self._buffer_idx += 1
if self._buffer_idx >= self.delay:
self._buffer_idx = 0
return output
def process_xcore(self, sample_int): # type: ignore : overloads base class
"""
Apply a comb filter to a signal, using fixed point maths.
Take one new sample and return the filtered sample.
Input should be scaled with 0 dB = 2**Q_SIG.
Parameters
----------
sample_int : int
Input sample as an integer.
"""
assert isinstance(sample_int, int), "Input sample must be an integer"
output = self._buffer_int[self._buffer_idx]
# do state calculation in int64 accumulator so we only quantize once
filtstore_64 = utils.int64(
output * self.damp2_int + self._filterstore_int * self.damp1_int
)
self._filterstore_int = rvb.scale_sat_int64_to_int32_floor(filtstore_64)
# do buffer calculation in int64 accumulator so we only quantize once
new_buff = utils.int64(
(sample_int << rvb.Q_VERB) + self._filterstore_int * self.feedback_int
)
self._buffer_int[self._buffer_idx] = rvb.scale_sat_int64_to_int32_floor(new_buff)
self._buffer_idx += 1
if self._buffer_idx >= self.delay:
self._buffer_idx = 0
return output
class reverb_room(rvb.reverb_base):
"""Generate a room reverb effect. This is based on Freeverb by
Jezar at Dreampoint, and consists of 8 parallel comb filters fed
into 4 series all-pass filters.
Parameters
----------
max_room_size : float, optional
sets the maximum size of the delay buffers, can only be set
at initialisation.
room_size : float, optional
how big the room is as a proportion of max_room_size. This
sets delay line lengths and must be between 0 and 1.
decay : int, optional
The length of the reverberation of the room, between 0 and 1.
damping : float, optional
how much high frequency attenuation in the room, between 0 and 1
Attributes
----------
comb_lengths : np.ndarray
An array of the comb filter delay line lengths, scaled by
max_room_size.
ap_length : np.ndarray
An array of the all pass filter delay line lengths, scaled by
max_room_size.
combs : list
A list of comb_fv objects containing the comb filters for the
reverb.
allpasses : list
A list of allpass_fv objects containing the all pass filters for
the reverb.
room_size : float
decay : float
feedback : float
feedback_int : int
feedback as a fixed point integer.
damping : float
damping_int : int
damping as a fixed point integer.
"""
def __init__(
self,
fs,
n_chans,
max_room_size=1,
room_size=1,
decay=0.5,
damping=0.4,
wet_gain_db=-1,
dry_gain_db=-1,
pregain=0.015,
predelay=10,
max_predelay=None,
Q_sig=dspg.Q_SIG,
):
assert n_chans == 1, f"Reverb room only supports 1 channel. {n_chans} specified"
# initalise wet/dry gains and predelay
super().__init__(
fs, n_chans, wet_gain_db, dry_gain_db, pregain, predelay, max_predelay, Q_sig
)
self._effect_gain = sc.fixed_gain(fs, 1, 10)
# the magic freeverb delay line lengths are for 44.1kHz, so
# scale them with sample rate and room size
default_comb_lengths = np.array([1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617])
default_ap_lengths = np.array([556, 441, 341, 225])
# buffer lengths
length_scaling = self.fs / 44100 * max_room_size
self.comb_lengths = (default_comb_lengths * length_scaling).astype(int)
self.ap_lengths = (default_ap_lengths * length_scaling).astype(int)
# feedbacks
init_fb = 0.5
init_damping = 0.4
self.combs = [
comb_fv(self.comb_lengths[0], init_fb, init_damping),
comb_fv(self.comb_lengths[1], init_fb, init_damping),
comb_fv(self.comb_lengths[2], init_fb, init_damping),
comb_fv(self.comb_lengths[3], init_fb, init_damping),
comb_fv(self.comb_lengths[4], init_fb, init_damping),
comb_fv(self.comb_lengths[5], init_fb, init_damping),
comb_fv(self.comb_lengths[6], init_fb, init_damping),
comb_fv(self.comb_lengths[7], init_fb, init_damping),
]
feedback_ap = 0.5
self.allpasses = [
allpass_fv(self.ap_lengths[0], feedback_ap),
allpass_fv(self.ap_lengths[1], feedback_ap),
allpass_fv(self.ap_lengths[2], feedback_ap),
allpass_fv(self.ap_lengths[3], feedback_ap),
]
# set filter delays
self.decay = decay
self.damping = damping
self.room_size = room_size
def reset_state(self):
"""Reset all the delay line values to zero."""
for cb in self.combs:
cb.reset_state()
for ap in self.allpasses:
ap.reset_state()
self._predelay.reset_state()
def get_buffer_lens(self):
"""Get the total length of all the buffers used in the reverb."""
total_buffers = 0
for cb in self.combs:
total_buffers += cb._max_delay
for ap in self.allpasses:
total_buffers += ap._max_delay
return total_buffers
@_deprecated(
"1.0.0", "2.0.0", "Replace `reverb_room.set_pre_gain(x)` with `reverb_room.pregain = x`"
)
def set_pre_gain(self, pre_gain):
"""
Set the pre gain.
Parameters
----------
pre_gain : float
pre gain value, less than 1.
"""
self.pregain = pre_gain
@_deprecated(
"1.0.0", "2.0.0", "Replace `reverb_room.set_wet_gain(x)` with `reverb_room.wet_db = x`"
)
def set_wet_gain(self, wet_gain_db):
"""
Set the wet gain.
Parameters
----------
wet_gain_db : float
Wet gain in dB, less than 0 dB.
"""
self.wet_db = wet_gain_db
@_deprecated(
"1.0.0", "2.0.0", "Replace `reverb_room.set_dry_gain(x)` with `reverb_room.dry_db = x`"
)
def set_dry_gain(self, dry_gain_db):
"""
Set the dry gain.
Parameters
----------
dry_gain_db : float
Dry gain in dB, lees than 0 dB.
"""
self.dry_db = dry_gain_db
@property
def decay(self):
"""The length of the reverberation of the room, between 0 and 1."""
ret = (self.feedback - 0.7) / 0.28
return ret
@decay.setter
def decay(self, x):
if not (0 <= x <= 1):
bad_x = x
x = np.clip(x, 0, rvb._LESS_THAN_1)
warnings.warn(f"Decay {bad_x} saturates to {x}", UserWarning)
self.feedback = x * 0.28 + 0.7
@property
def feedback(self):
"""Gain of the feedback line in the reverb filters. Set decay to update this value."""
ret = float(self.combs[0].feedback)
return ret
@feedback.setter
def feedback(self, x):
for cb in self.combs:
cb.set_feedback(x)
self.feedback_int = self.combs[0].feedback_int
@_deprecated(
"1.0.0", "2.0.0", "Replace `reverb_room.set_decay(x)` with `reverb_room.decay = x`"
)
def set_decay(self, decay):
"""
Set the decay of the reverb.
Parameters
----------
decay : float
How long the reverberation of the room is, between 0 and 1.
"""
self.decay = decay
@property
def damping(self):
"""How much high frequency attenuation in the room, between 0 and 1."""
return self.combs[0].damp1
@damping.setter
def damping(self, x):
if not (0 <= x <= 1):
bad_x = x
x = np.clip(x, 0, 1)
warnings.warn(f"Pregain {bad_x} saturates to {x}", UserWarning)
for cb in self.combs:
cb.set_damping(x)
self.damping_int = self.combs[0].damp1_int
@_deprecated(
"1.0.0", "2.0.0", "Replace `reverb_room.set_damping(x)` with `reverb_room.damping = x`"
)
def set_damping(self, damping):
"""
Set the damping of the reverb.
Parameters
----------
damping : float
How much high frequency attenuation in the room, between 0 and 1.
"""
self.damping = damping
@property
def room_size(self):
"""The room size as a proportion of the max_room_size."""
return self._room_size
@room_size.setter
def room_size(self, x):
if not (0 <= x <= 1):
raise ValueError(
"room_size must be between 0 and 1. For larger rooms, increase max_room size"
)
self._room_size = x
comb_delays = (self.comb_lengths * self._room_size).astype(int)
ap_delays = (self.ap_lengths * self._room_size).astype(int)
for n in range(len(self.combs)):
self.combs[n].set_delay(comb_delays[n])
for n in range(len(self.allpasses)):
self.allpasses[n].set_delay(ap_delays[n])
@_deprecated(
"1.0.0", "2.0.0", "Replace `reverb_room.set_room_size(x)` with `reverb_room.room_size = x`"
)
def set_room_size(self, room_size):
"""
Set the current room size; will adjust the delay line lengths accordingly.
Parameters
----------
room_size : float
How big the room is as a proportion of max_room_size. This
sets delay line lengths and must be between 0 and 1.
"""
self.room_size = room_size
def process(self, sample, channel=0):
"""
Add reverberation to a signal, using floating point maths.
Take one new sample and return the sample with reverb.
Input should be scaled with 0 dB = 1.0.
"""
delayed_input = self._predelay.process_channels([sample])[0]
reverb_input = delayed_input * self.pregain
output = 0
for cb in self.combs:
output += cb.process(reverb_input)
for ap in self.allpasses:
output = ap.process(output)
output = self._effect_gain.process(output * self.wet) + sample * self.dry
return output
def process_xcore(self, sample, channel=0):
"""
Add reverberation to a signal, using fixed point maths.
Take one new sample and return the sample with reverb.
Input should be scaled with 0 dB = 1.0.
"""
sample_int = utils.float_to_fixed(sample, self.Q_sig)
delayed_input = self._predelay.process_channels_xcore([sample_int])[0]
reverb_input = rvb.apply_gain_xcore(delayed_input, self.pregain_int)
output = 0
for cb in self.combs:
output += cb.process_xcore(reverb_input)
utils.int64(output)
output = utils.saturate_int64_to_int32(output)
# these buffers are at risk of overflowing, but self.gain_int
# should be scaled to prevent it for nearly all signals
for ap in self.allpasses:
output = ap.process_xcore(output)
utils.int32(output)
# need an extra bit in this add, if wet/dry mix is badly set
# output can saturate (users fault)
output = rvb.apply_gain_xcore(output, self.wet_int)
output = self._effect_gain.process_xcore(output)
output += rvb.apply_gain_xcore(sample_int, self.dry_int)
utils.int64(output)
output = utils.saturate_int64_to_int32(output)
return utils.fixed_to_float(output, self.Q_sig)