532 lines
17 KiB
Python
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)
|