Impulse Response (IR): Plotting Functions

Impulse Response (IR): Plotting Functions#

import sys
# sys.path.append('../')
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
from scipy.io import wavfile
from scipy import signal
mpl.rcParams['lines.linewidth'] = 1         # Set the default linewidth  
mpl.rcParams['font.size'] = 10              # Set the default linewidth
mpl.style.use('default')                                     
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 3
      1 import sys
      2 # sys.path.append('../')
----> 3 import numpy as np
      4 import matplotlib as mpl
      5 import matplotlib.pyplot as plt

ModuleNotFoundError: No module named 'numpy'

We read the IR of the BX25 spring reverb and plot it with different templates.

input_path = "./audio/IR_AKG_BX25_3500ms_48kHz24b.wav"
sample_rate, data = wavfile.read(input_path)
# data, sample_rate = sf.read(input_path)
print(f'Input dtype: {data.dtype}, sample rate: {sample_rate}')
print(f'Input shape: {data.shape}, min:{data.min():.6f}, max:{data.max():.6f}, mean:{data.mean():.6f}')
Input dtype: int32, sample rate: 48000
Input shape: (269190,), min:-2147483648.000000, max:1472520704.000000, mean:-2165.042565
"""
DC offset removal and normalization:
This file has been already normalized and the DC offset removed.
These function are here just for reference, but they are not in the pipeline.
"""

data = data - data.mean()            # Remove DC offset

data = data / np.abs(data).max()   # Normalize to [-1, 1]

Waveform with Zoom#

def plot_impulse_with_zoom(data, sample_rate, zoom_factor=0.1):
    """
    Plot the waveform and zoom in on the impulse.

    Parameters:
    - data: The impulse signal data.
    - sample_rate: The sample rate of the data.
    - zoom_factor: The fraction of the total duration to show around the impulse.
    """

    # Identify where the impulse is (find the sample with the highest absolute amplitude)
    impulse_index = np.argmax(np.abs(data))

    # Compute the number of samples to show around the impulse for zooming
    samples_to_show = int(sample_rate * zoom_factor)

    # Define start and end indices for the zoomed view
    start_index = max(0, impulse_index - samples_to_show // 2)
    end_index = min(len(data) - 1, impulse_index + samples_to_show // 2)

    # Create plots
    fig, axs = plt.subplots(2, 1, figsize=(8, 5))

    # Full waveform
    axs[0].plot(data, linewidth=0.75)
    axs[0].set_title("Full Waveform")
    axs[0].set_xlabel("Samples")
    axs[0].set_ylabel("Amplitude")
    axs[0].grid()

    # Zoomed-in waveform
    axs[1].plot(range(start_index, end_index), data[start_index:end_index], linewidth=0.75)
    axs[1].set_title("Zoomed-In on Impulse")
    axs[1].set_xlabel("Samples")
    axs[1].set_ylabel("Amplitude")
    axs[1].grid()

    plt.tight_layout()
    plt.show()

# Test with the impulse signal from the previous code snippet
plot_impulse_with_zoom(data, sample_rate)
../_images/848147e81277e0d8bd4f1765973159e0552fb07e9138625e2b39a43577add3e8.png

Waveform, Zoom and Spectrogram#

def plot_impulse_and_spectrogram(data, sample_rate, zoom_factor=0.5):
    """
    Plot the waveform, zoom in on the impulse, and display its traditional spectrogram.

    Parameters:
    - data: The impulse signal data.
    - sample_rate: The sample rate of the data.
    - zoom_factor: The fraction of the total duration to show around the impulse.
    """
    # Identify where the impulse is (find the sample with the highest absolute amplitude)
    impulse_index = np.argmax(np.abs(data))

    # Compute the number of samples to show around the impulse for zooming
    samples_to_show = int(sample_rate * zoom_factor)

    # Define start and end indices for the zoomed view
    start_index = max(0, impulse_index - samples_to_show // 2)
    end_index = min(len(data) - 1, impulse_index + samples_to_show // 2)

    # Extract zoomed data
    zoomed_data = data[start_index:end_index]

   # Compute the spectrogram of the zoomed data with a larger window for better frequency resolution
    signal.get_window('blackman', 256)
    # nperseg = int(0.1 * sample_rate)
    f, t, Sxx = signal.spectrogram(zoomed_data, fs=sample_rate, window='blackman', mode='magnitude', scaling='spectrum')

    # Create plots
    fig, axs = plt.subplots(3, 1, figsize=(10, 10))

    # Full waveform
    axs[0].plot(data)
    axs[0].set_title("Full Waveform")
    axs[0].set_xlabel("Samples")
    axs[0].set_ylabel("Amplitude")
    axs[0].grid()

    # Zoomed-in waveform
    axs[1].plot(range(start_index, end_index), zoomed_data)
    axs[1].set_title("Zoomed-In on Impulse")
    axs[1].set_xlabel("Samples")
    axs[1].set_ylabel("Amplitude")
    axs[1].grid()

    # Traditional spectrogram
    # cmap = plt.get_cmap('inferno')
    Sxx_dB = 10 * np.log10(np.abs(Sxx))
    max_mag = np.max(Sxx_dB)
    min_mag = max_mag - 70              # Dynamic range of 60 dB
    img = axs[2].pcolormesh(t, f, Sxx_dB, shading='gouraud', cmap='inferno', norm=plt.Normalize(vmin=min_mag, vmax=max_mag))
    axs[2].set_yscale('log')            # Logarithmic scale for frequencies
    axs[2].set_ylim(f[1], f[-1])        # Exclude DC (0 Hz)
    fig.colorbar(img, ax=axs[2], format="%+2.0f dB")
    axs[2].set_title("Spectrogram")
    axs[2].set_ylabel("Frequency [Hz]")
    axs[2].set_xlabel("Time [sec]")
    
    # Set x-axis ticks
    custom_ticks = [20,100, 200, 400, 1000, 2000, 4000, 8000]
    plt.yticks(custom_ticks, custom_ticks)

    plt.tight_layout()
    plt.show()
plot_impulse_and_spectrogram(data, sample_rate, zoom_factor=0.9)
../_images/407f324fca4e8688463c3d59a449c7a8143f8f0e13985e5239f0613f2ee27119.png

Waterfall Plot#

def generate_spectrogram(waveform, sample_rate):
    frequencies, times, Sxx = signal.spectrogram(
        waveform, 
        fs=sample_rate, 
        window='blackmanharris',
        nperseg=32,
        noverlap=16,  
        scaling='spectrum', 
        mode='magnitude'
    )
    
    # Convert magnitude to dB
    Sxx_dB = 10 * np.log10(Sxx + 1e-10)
    
    return frequencies, times, Sxx_dB

def plot_waterfall(waveform, title, sample_rate, stride=1):
    frequencies, times, Sxx = generate_spectrogram(waveform, sample_rate)

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

    X, Y = np.meshgrid(frequencies, times[::stride])
    Z = Sxx.T[::stride]

    surf = ax.plot_surface(X, Y, Z, 
                           cmap='inferno',
                           edgecolor='none', 
                           alpha=0.8,
                           linewidth=0,
                            antialiased=False)

    # Add a color bar which maps values to colors
    ax.autoscale()  # Adjusts the viewing limits for better visualization
    cbar = fig.colorbar(surf, ax=ax, pad=0.01, aspect=35, shrink=0.5)
    cbar.set_label('Magnitude (dB)')

    # Set labels and title
    ax.set_xlabel('Frequency (Hz)')
    ax.set_ylabel('Time (seconds)')
    ax.set_zlabel('Magnitude (dB)')
    # ax.set_title(title)
    
    ax.set_xlim([frequencies[-1], frequencies[0]])
    # ax.set_xscale('symlog', linthreshx=0.01)
    # ax.set_xscale('log')
    # ax.set_xlim([20000, 20])  # Set the x-axis limit to be between 20 and 20,000 Hz in log scale
    ax.view_init(elev=10, azim=45, roll=None, vertical_axis='z')  # Adjusts the viewing angle for better visualization
    
    plt.tight_layout()
title = 'Waterfall Plot of the Impulse Response'
data = data
index = len(data) // 2
input = data[index:]
print(input.shape, index)
plot_waterfall(input, title, sample_rate, stride=1)
(134595,) 134595
../_images/5f44708135d50a78058c7bfee6c465a1c8d044723e1a5fceed2072cd6537b781.png
signal.get_window('blackman', 512)

impulse_response = input.reshape(-1)
print(impulse_response.shape, impulse_response.min(), impulse_response.max(), impulse_response.mean())
frequencies, times, Sxx = signal.spectrogram(impulse_response, fs=sample_rate, window='blackman', nperseg=1024, noverlap=512, mode='magnitude', scaling='spectrum')

# Convert the magnitude to dB scale:
Sxx_dB = 10 * np.log10(np.abs(Sxx) + 1e-10)

fig, ax = plt.subplots(figsize=(5, 5))

cax = ax.pcolormesh(times, frequencies, Sxx_dB, antialiased=True, shading='gouraud', cmap='inferno', norm=plt.Normalize(vmin=-90, vmax=-20))

# Add the colorbar
cbar = fig.colorbar(mappable=cax, ax=ax, format="%+2.0f dB")

ax.grid(True)
cbar.set_label('Intensity [dB]')
ax.set_ylabel('Frequency [Hz]')
ax.set_xlabel('Time [sec]')
ax.set_ylim(0, 20000)
plt.tight_layout()
plt.show()
(134595,) -0.006336402462802971 0.005951703492671309 2.352945489334609e-07
../_images/307bb054a1934d5e9fa77ff3aa7fb98c435f1ecedfecd1ffba28e176a649f921.png
sample_rate, sweep = wavfile.read('./audio/log_sweep_tone.wav')
print(f'Sweep dtype: {sweep.dtype}, sample rate: {sample_rate}')
print(f'Sweep shape: {sweep.shape}, min:{sweep.min():.6f}, max:{sweep.max():.6f}, mean:{sweep.mean():.6f}', 'end=\n\n')

sample_rate, impulse_response = wavfile.read('./audio/ir_reference.wav')
print(f'IR dtype: {impulse_response.dtype}, sample rate: {sample_rate}')
print(f'IR shape: {impulse_response.shape}, min:{impulse_response.min():.6f}, max:{impulse_response.max():.6f}, mean:{impulse_response.mean():.6f}')
Sweep dtype: int32, sample rate: 48000
Sweep shape: (240000,), min:-2147483648.000000, max:2147483392.000000, mean:3418140.397867 end=


IR dtype: int32, sample rate: 48000
IR shape: (479999,), min:-7513856.000000, max:2147483392.000000, mean:45.643828
plot_impulse_and_spectrogram(sweep, sample_rate, zoom_factor=0.5)
../_images/3d0a8bacfc14918ae746303d0f736e5043a9ad08bcedca3421295315f7e97df2.png
plot_impulse_and_spectrogram(impulse_response, sample_rate, zoom_factor=0.5)
../_images/cd726780d58f8461f4e6369bbf631d80dbc96153e5ecd29cda4ac8c71e7d62bf.png