Reverberation#

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
from scipy.io import wavfile

plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['axes.grid'] = True
np.random.seed(42)

db_to_mag = lambda x: 10 ** (x / 20)
mag_to_db = lambda x: 20 * np.log10(x)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 import numpy as np
      2 import matplotlib.pyplot as plt
      3 from scipy import signal

ModuleNotFoundError: No module named 'numpy'

Generate a synthetic room impulse response (RIR)#

# Generate time axis (in milliseconds)
time_array = np.linspace(0, 400, 250)

# Exponential decay with noise
rir = np.exp(-0.01 * time_array) * (1 + 0.1 * np.random.randn(len(time_array)))
rir = np.clip(rir, 0, None)  # Ensure no negative values
rir /= np.max(np.abs(rir))  # Normalize to 1
# Define colormap and colors
cmap = plt.get_cmap("RdYlBu") 
colors = [cmap(0.1), cmap(0.4), cmap(0.8), cmap(1.0)]  # Select spaced points along the colormap
# Define exact transition points in milliseconds
direct_end = 20
early_reflections_end = 40
cluster_end = 100
late_reverb_start = 100

# Define regions for coloring
direct_region = (time_array <= direct_end)
early_reflections_region = (time_array > direct_end) & (time_array <= early_reflections_end)
cluster_region = (time_array > early_reflections_end) & (time_array <= cluster_end)
late_reverb_region = (time_array > late_reverb_start)


# Plotting the waveform
plt.figure(figsize=(10, 5))

# Fill regions with distinct colors
plt.fill_between(
    time_array, rir, where=direct_region, 
    color=colors[0], alpha=0.8, label="Direct Signal"
)
plt.fill_between(
    time_array, rir, where=early_reflections_region, 
    color=colors[1], alpha=0.8, label="Early Reflections"
)
plt.fill_between(
    time_array, rir, where=cluster_region, 
    color=colors[2], alpha=0.8, label="Cluster"
)
plt.fill_between(
    time_array, rir, where=late_reverb_region, 
    color=colors[3], alpha=0.8, label="Late Reverberation"
)

# General plot settings
plt.ylim(0, 1)
plt.xlim(-10, 400)
plt.xlabel("Time (ms)", fontsize=12)
plt.ylabel("Amplitude", fontsize=12)
plt.xticks([0, 20, 40, 100, 400])
plt.title("Temporal Structure (Continuous Time)", fontsize=14)
plt.legend()
# plt.grid(alpha=0.3)
plt.show()
../_images/e63a6edd689a6c84631d42c80dd2218ca3e9023adc4e62beb07058b50e8e9868.png
# Define exact transition points in milliseconds
direct_end = 20
early_reflections_end = 40
cluster_end = 100
late_reverb_start = 100

# Define regions for coloring
direct_region = (time_array <= direct_end)
early_reflections_region = (time_array > direct_end) & (time_array <= early_reflections_end)
cluster_region = (time_array > early_reflections_end) & (time_array <= cluster_end)
late_reverb_region = (time_array > late_reverb_start)

# Plotting the discrete version
plt.figure(figsize=(10, 5))

# Direct signal
plt.stem(
    time_array[direct_region],
    rir[direct_region],
    linefmt=colors[0],
    markerfmt="",
    basefmt=" ",
    label="Direct Signal",
).markerline.set_markersize(4)

# Early reflections
plt.stem(
    time_array[early_reflections_region],
    rir[early_reflections_region],
    linefmt=colors[1],
    markerfmt="",
    basefmt=" ",
    label="Early Reflections",
).markerline.set_markersize(4)

# Cluster
plt.stem(
    time_array[cluster_region],
    rir[cluster_region],
    linefmt=colors[2],
    markerfmt="",
    basefmt=" ",
    label="Cluster",
).markerline.set_markersize(4)

# Late reverberation
plt.stem(
    time_array[late_reverb_region],
    rir[late_reverb_region],
    linefmt=colors[3],
    markerfmt="",
    basefmt=" ",
    label="Late Reverberation",
).markerline.set_markersize(4)

# General plot settings
plt.ylim(0, 1.)
plt.xlim(-10, 400)
plt.xlabel("Time (ms)", fontsize=12)
plt.ylabel("Amplitude", fontsize=12)
plt.xticks([0, 20, 40, 100, 400])
plt.title("Temporal Structure (Discrete Time)", fontsize=14)
plt.legend()
# plt.grid(alpha=0.3)
plt.show()
../_images/6e4c4e37c5c2a078b063202de4eaea34219c47fd7517274f15cf6f868f15a864.png

Plot the Waterfall plot of the RIR#

def generate_spectrogram(waveform, sample_rate, magnitude_range=(-70, 0)):
    # Compute the spectrogram
    frequencies, times, Sxx = signal.spectrogram(
        waveform, 
        fs=sample_rate, 
        window='blackmanharris',
        nperseg=256,  # Adjusted for better frequency resolution
        noverlap=128,  # Adjusted for overlap
        scaling='spectrum', 
        mode='magnitude'
    )
    
    # Convert magnitude to dB
    Sxx_dB = 10 * np.log10(Sxx + 1e-10)
    
    # Apply magnitude range limits
    min_db, max_db = magnitude_range
    Sxx_dB = np.clip(Sxx_dB, min_db, max_db)
    
    return frequencies, times, Sxx_dB

def plot_waterfall(waveform, title, sample_rate, stride=1, time_range=None, magnitude_range=(-70, 0)):
    frequencies, times, Sxx = generate_spectrogram(waveform, sample_rate, magnitude_range)

    # Apply the specified time range if provided
    if time_range is not None:
        start_time, end_time = time_range
        if end_time > times[-1]:  # Extend waveform if time range exceeds signal duration
            padding_length = int((end_time - times[-1]) * sample_rate)
            waveform = np.concatenate((waveform, np.zeros(padding_length)))
            frequencies, times, Sxx = generate_spectrogram(waveform, sample_rate, magnitude_range)
        time_mask = (times >= start_time) & (times <= end_time)
        times = times[time_mask]  # Filter times
        Sxx = Sxx[:, time_mask]  # Filter corresponding spectrogram values

    fig = plt.figure(figsize=(15, 8))
    ax = fig.add_subplot(111, projection='3d')

    # Create the meshgrid for the waterfall plot
    X, Y = np.meshgrid(times[::stride], frequencies)  # Flip X and Y to match time-frequency order
    Z = Sxx[:, ::stride]  # Corresponding slice for Z

    # Plot the surface
    surf = ax.plot_surface(X, Y, Z, 
                           cmap='jet',
                           edgecolor='none', 
                           alpha=1.0)

    # Add a color bar
    cbar = fig.colorbar(surf, ax=ax, pad=0.01, aspect=30, shrink=0.5)
    cbar.set_label('Magnitude (dB)')

    # Set axis labels and title
    ax.set_xlabel('Time (seconds)', labelpad=10)
    ax.set_ylabel('Frequency (Hz)', labelpad=10)
    ax.set_zlabel('Magnitude (dB)', labelpad=10)
    ax.set_title(title, pad=20)

    # Flip the y-axis (frequency) if needed to show low-to-high
    ax.set_ylim([frequencies[0], frequencies[-1]])
    ax.set_xlim([times[-1], times[0]])

    # Adjust the view angle for better visualization
    ax.view_init(elev=45, azim=-110)

    plt.tight_layout()
    plt.show()

# Generate a synthetic impulse response
time_array = np.linspace(0, 1, 48000)  # 1-second signal at 48 kHz

# Create a decaying exponential for the first half
rir = np.zeros_like(time_array)  # Initialize the full signal with silence
half_length = len(time_array) // 2
rir[:half_length] = np.exp(-20 * time_array[:half_length]) * (1 + 0.01 * np.random.randn(half_length))  # Fill only half

# Optional: Normalize to ensure proper scaling
rir /= np.max(np.abs(rir))

# Use the extended signal for visualization
title = 'Waterfall Plot of Synthetic Impulse Response'

# Specify a custom time range in seconds and magnitude range
time_range = (0., 0.6)  # Visualize from 0.5 to 1.5 seconds (beyond the signal duration)
magnitude_range = (-70, 0)  # Limit magnitude to the range [-70, 0] dB
plot_waterfall(rir, title, 48000, stride=2, time_range=time_range, magnitude_range=magnitude_range)
../_images/d6fc9739646d02e19325bc2d0634965bf9020f333e53604c07726ba07050ef58.png
# Define colormap and colors
cmap = plt.get_cmap("RdYlBu") 
num_colors = 3

# select points in the colormap and add them to a list
points_colors = np.linspace(0, 1, num_colors)
colors = [cmap(point) for point in points_colors]

# Parameters
RT_0 = 500  # Reverberation Time (ms)
time = np.linspace(0, (2 * RT_0), RT_0) # Time vector (ms)

# Crossover frequencies
freq_low = 250  # Low frequency (Hz)
freq_hi = 6000  # High frequency (Hz)

room_size = 2000  # Room size in cubic meters

# Relative decay for low, mid, and high frequencies
rt_low = 0.4
rt_mid = 0.2
rt_hi = 0.1 

# # Gains in dBFS
direct_gain = db_to_mag(0.0)
early_gain = db_to_mag(-8.5)
late_gain = db_to_mag(0.0)

# Calculate decay functions ensuring they reach 0 at the end of RT_0
low_decay = np.exp(-time / (RT_0 * rt_low))
mid_decay = np.exp(-time / (RT_0 * rt_mid))
hi_decay = np.exp(-time / (RT_0 * rt_hi))

# Plot
plt.figure(figsize=(12, 6))

plt.fill_between(time, low_decay, color=colors[0], alpha=0.8, label="Low Frequencies")
plt.fill_between(time, mid_decay, color=colors[1], alpha=0.8, label="Mid Frequencies")
plt.fill_between(time, hi_decay, color=colors[2], alpha=0.8, label="High Frequencies")

# Plot settings
plt.title("Decay of the Frequency Bands", fontsize=14)
plt.xlabel("Time (ms)", fontsize=12)
plt.ylabel("Amplitude (Linear Scale)", fontsize=12)
plt.xlim(0, RT_0)
plt.ylim(0, 1.1)
plt.legend()
plt.tight_layout()
plt.show()
../_images/ddddc9c9ae20513b4b8a9827c5c61f7f03d58fd4373824b9caf2a8824f24ce35.png