Assignment 5: Artificial Reverberation#
(1) Friedrich-Alexander-Universität Erlangen-Nürnberg (2) Aalto University
Contact: sebastian.schlecht@fau.de, cristobal.andrade@fau.de
In this assignment, your task is to implement a late reverberation algorithm based on feedback delay networks (FDNs).
Duration: 12 Hours
Requirements: DSP
Dependencies pip install numpy scipy matplotlib soundfile pyfar jupyter pooch watermark
[ ]:
import numpy as np
import matplotlib.pyplot as plt
import pooch
import os
from scipy.signal import freqz
from scipy.io import wavfile
from scipy.linalg import hadamard
Let’s start by downloading all the necessary files for running this assignment.
[ ]:
# Leave this as it is: This is the URL from which the data will be downloaded
url = 'https://github.com/pyfar/open-educational-resources/tree/main/courses/Virtual_Acoustics_Lab_FAU/Assignment5'
# Get current working directory (where the notebook was started)
notebook_dir = os.getcwd()
# Create a Pooch object using that directory
my_pooch = pooch.create(
path=notebook_dir,
base_url=url, # Change this to your actual URL
registry={
"SOSarray_DSP.py" : None,
"Config.py" : None,
"DSP.py" : None,
}
)
# Download all files
for fname in my_pooch.registry:
fpath = my_pooch.fetch(fname)
print(f"Downloaded: {fpath}")
Now let’s import all the necessary files.
[ ]:
from Config import Config
from DSP import m2smp
from SOSarray_DSP import SOSarray_DSP
%matplotlib inline
config = Config()
Create impulse signal#
[ ]:
numBlocks = 300
signalLength = config.blockSize * numBlocks
signal = np.zeros((signalLength, 1))
signal[0, 0] = 1
time = np.arange(1, signalLength + 1) / config.fs
1 TASK: Compute delay-proportional absorption filters#
- Compute filter for 100 samples delay. For this implement
onePoleAbsorptionanddesignOnePoleFilter.Hint: Recall the frequency response for a one-pole filter:\[H(e^{j\omega T}) = \frac{b_0}{1 + a_1 e^{-j\omega T}}\]Think about a way of solving this for \(a_1\) and \(b_0\) given the magnitude values at DC and Nyquist.
- Plot the filter magnitude response
- Convert filter magnitude response to attenuation per sample
- Plot reverberation time corresponding to this attenuation per sample
- Repeat a)-d) for 1000 samples delay
Explain the plots
[ ]:
def designOnePoleFilter(HDc, HNyq):
"""
Design one-pole filter
Inputs:
HDc - Linear magnitude at DC of size [1, number of filters] or [number of filters]
HNyq - Linear magnitude at Nyquist of size [1, number of filters] or [number of filters]
Outputs:
sos - sos filters of size [6 x number of filters]
Example:
sos = designOnePoleFilter(np.random.rand(3), np.random.rand(3))
"""
# Ensure inputs are numpy arrays
HDc = np.atleast_1d(HDc)
HNyq = np.atleast_1d(HNyq)
numFilters = HDc.size
sos = np.zeros((6, numFilters))
sos[3, :] = 1 # a0 = 1
# TASK: Implement one-pole filter
# a) See: https://ccrma.stanford.edu/~jos/fp/One_Pole.html
# b) Solve for a1 and b0 (e.g. by plugging in HDc and HNyq and rearranging)
# c) Implement the filter coefficients into second-order sections (SOSs)
# [b0; b1; b2; a0; a1; a2], where a0 = 1.
# d) Vectorize for multiple filters.
# YOUR CODE HERE
raise NotImplementedError()
return sos
def onePoleAbsorption(RT_DC, RT_NY, delays, fs):
"""
Design one-pole absorption filters according to specified T60
Syntax: sos = onePoleAbsorption(RT_DC, RT_NY, delays, fs)
Inputs:
RT_DC - Reverberation time in [seconds] at DC frequency
RT_NY - Reverberation time in [seconds] at Nyquist frequency
delays - Array of delay times in [samples]
fs - Sampling frequency
Outputs:
sos - Filter coefficients of size [6, 1, N] where N is number of filters
Example:
sos = onePoleAbsorption(2, 1, np.array([100, 130]), 48000)
"""
# Ensure delays is a numpy array
delays = np.atleast_1d(delays)
# Dummy
N = delays.size
sos = np.zeros((6, 1, N))
sos[[0, 3], :, :] = 1
# Calculate magnitude response at DC and Nyquist
# Hint: RT60 to slope conversion: -60 dB / (RT60 * fs) gives dB per sample
# YOUR CODE HERE
raise NotImplementedError()
# Design one-pole filters
sos = designOnePoleFilter(HDc, HNyq)
# conform to shape [6, 1, N]
sos = sos[:, np.newaxis, :]
return sos
delayLengths = np.array([100])
# Compute filter for 100 samples delay
# YOUR CODE HERE
raise NotImplementedError()
# Convert filter magnitude response to attenuation per sample
# YOUR CODE HERE
raise NotImplementedError()
delayLengths = np.array([1000])
# Repeat the same steps for 1000 samples delay
# YOUR CODE HERE
raise NotImplementedError()
# Plot the filter magnitude response
# YOUR CODE HERE
raise NotImplementedError()
# Plot the reverberation time
# YOUR CODE HERE
raise NotImplementedError()
2 TASK: Implement a lossless feedback matrix#
Generate a lossless matrix of size
numberOfDelaysProve numerically the losslessness
Repeat a) and b) for another type of matrix
Explain differences between those feedback matrices, e.g., density, computational cost.
[ ]:
numberOfDelays = config.Late.numberOfDelays
# YOUR CODE HERE
raise NotImplementedError()
# Set feedback matrix for a later task
config.Late.feedbackMatrix = A
# Explain diffrences
# YOUR CODE HERE
raise NotImplementedError()
3 TASK: Initialize the FDN parameters#
Compute delay line lengths from
config.roomSizeInitialize and set a multichannel delay with correct lengths. Use
dsp.Delay(delayLengths), where delayLengths is [1xnCh] vector in samples.Compute absorption filters
Initialize a SOSarray_DSP with filters
Generate input and output gains (can be ones or random)
Set the lossless feedback matrix
[ ]:
blockSize = config.blockSize
RT60 = config.RT60
fs = config.fs
# Compute delay line lengths
# YOUR CODE HERE
raise NotImplementedError()
# Init delay buffers and indices arrays
# YOUR CODE HERE
raise NotImplementedError()
# Compute absorption filters and set them in the SOSarray_DSP
# YOUR CODE HERE
raise NotImplementedError()
# Set input and output gains
# YOUR CODE HERE
raise NotImplementedError()
# Set feedback matrix
# YOUR CODE HERE
raise NotImplementedError()
4 TASK: Implement the late reverberation processing with a Feedback Delay Network.#
Before implementing a full Feedback Delay Network (FDN), we’ll start with a simpler building block: a one-pole feedback comb filter. This filter has the transfer function:
where \(g\) is the feedback gain and \(D\) is the delay length.
The processing structure we use here (circular buffers, block processing, feedback paths) is very similar to what you’ll implement for the FDN.
[ ]:
def comb_filter(signal: np.ndarray,
delayLength: int,
feedbackGain: float,
blockSize: int = 256) -> np.ndarray:
"""
Process a 1D feedback comb filter using block processing and a circular buffer.
Args:
signal: Input signal array of shape [num_samples, 1] or [num_samples].
delayLength: Delay line length in samples (integer, >= 1).
feedbackGain: Feedback gain scalar (typically |g| < 1 for stability).
blockSize: Block size for processing.
Returns:
Output signal array of shape [num_samples, 1].
"""
if signal.ndim == 1:
signal = signal.reshape(-1, 1)
numSamples = signal.shape[0]
# Delay state
delayBuffer = np.zeros(delayLength, dtype=np.float64)
delayWriteIndex = 0
# Processing buffers
lastBlock = np.zeros(blockSize, dtype=np.float64)
output = np.zeros((numSamples, 1), dtype=np.float64)
numBlocks = int(np.ceil(numSamples / blockSize))
for it_block in range(numBlocks):
block_index = slice(it_block * blockSize, (it_block + 1) * blockSize)
# Skip if not full block
if block_index.stop > numSamples:
break
block = signal[block_index, :]
# Output mixing: direct reference since we don't modify lastBlock
out = lastBlock
# Feedback and input injection
feedback_paths = lastBlock * feedbackGain # Direct scalar multiplication
delay_input = block[:, 0] + feedback_paths # Direct 1D operations
# Circular-buffer delay processing
delay_output = np.zeros(blockSize, dtype=np.float64)
# Single delay line processing
for i in range(blockSize):
read_idx = (delayWriteIndex - delayLength) % delayLength
delay_output[i] = delayBuffer[read_idx]
delayBuffer[delayWriteIndex] = delay_input[i]
delayWriteIndex = (delayWriteIndex + 1) % delayLength
# Update state for next block
lastBlock = delay_output
# Write output
output[block_index, 0] = out
return output
# Example usage
fs = 48000
duration = 1.0
t = np.arange(int(fs * duration)) / fs
# Impulse input
x = np.zeros_like(t)
x[0] = 1.0
y = comb_filter(x, delayLength=int(0.03 * fs), feedbackGain=0.6, blockSize=256)
# Plot input and output (impulse response)
plt.figure()
plt.plot(t, y, label='Output', alpha=0.9)
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.title('Feedback Comb Filter - Impulse Response')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Using a similar processing structure as the comb filter above
Implement a FDN processing loop that handles multiple delay lines simultaneously
Use lastBlock to retain signal between the for loop iterations

[ ]:
lastBlock = np.zeros((blockSize, numberOfDelays))
output = np.zeros((signalLength, 1))
for it_block in range(numBlocks):
block_index = slice(it_block * blockSize, (it_block + 1) * blockSize)
block = signal[block_index, :]
# Compute output using lastBlock and outputGains
# YOUR CODE HERE
raise NotImplementedError()
# Compute feedback paths and delay input
# YOUR CODE HERE
raise NotImplementedError()
# Implement delay lines using circular buffer
delay_output = np.zeros((blockSize, numberOfDelays))
# YOUR CODE HERE
raise NotImplementedError()
# Process delay lines through absorption filters
# YOUR CODE HERE
raise NotImplementedError()
lastBlock = delay_output
output[block_index, :] = out
5 TASK: describe the result#
Highlight sonic features such as smoothness, density, ringing.
How does the time-domain plot conform to your expectation of the reverb design. Highlight features such as early reflections and late reverberation.
How does the spectrogram conform to your expectation of the reverb design. Highlight features such as reverberation time.
[ ]:
# Save audio file
output_normalized = output / np.max(np.abs(output))
wavfile.write('assignment_Late_DSP.wav', config.fs, output_normalized.astype(np.float32))
# Plot and describe your results
# YOUR CODE HERE
raise NotImplementedError()
6 TASK: make one more FDN design#
Replace the input signal with something more interesting
Change at least one of these parameters: feedback matrix, room size, reverberation time, number of delays
Explain your change and the result
[ ]:
# YOUR CODE HERE
raise NotImplementedError()
License notice#
This notebook is licensed under CC BY 4.0
Watermark#
The following watermark might help others to install specific package versions that might be required to run the notebook.
[1]:
%load_ext watermark
%watermark -v -m -p numpy,scipy,matplotlib,soundfile,pyfar,sofar,jupyter,watermark
Python implementation: CPython
Python version : 3.12.9
IPython version : 8.12.3
numpy : 1.26.4
scipy : 1.15.2
matplotlib: 3.10.1
soundfile : 0.13.1
pyfar : 0.7.1
sofar : 1.2.1
jupyter : 1.1.1
watermark : 2.5.0
Compiler : Clang 13.0.0 (clang-1300.0.29.30)
OS : Darwin
Release : 24.5.0
Machine : arm64
Processor : arm
CPU cores : 12
Architecture: 64bit