init
This commit is contained in:
531
lib_audio_dsp/python/audio_dsp/dsp/reverb.py
Normal file
531
lib_audio_dsp/python/audio_dsp/dsp/reverb.py
Normal file
@@ -0,0 +1,531 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user