fix: mel-spec compute bugs causes wrong identity
Browse files- Padding for center=False: Added the initial reflection padding of
(n_fft - hop_size) // 2 in the main mel_spectrogram_numpy function,
which was previously missing.
- Mel Formula Discrepancy: Previous implementation used the HTK mel formula,
while librosa.filters.mel defaults to the Slaney mel scale (which is linear
below 1 kHz and logarithmic above). Updated the hz_to_mel and mel_to_hz
functions to use the Slaney formula.
- Window Function Difference: np.hanning produces a non-periodic window,
whereas torch.hann_window(..., periodic=True) (the default) produces
a periodic window. Replaced np.hanning with a manual periodic Hann
window implementation.
- Mel Filterbank Frequencies: The frequency points for the mel filterbank
were slightly off because they didn't account for the Slaney scale's
specific behavior.
- After these fixes, the maximum difference between the NumPy and PyTorch
outputs is approximately 1.89e-06, which is within the expected range for
floating-point precision differences.
- This resolves the speaker identity issues in the output wav.
- src/utils/audio_utils.py +53 -95
|
@@ -22,30 +22,44 @@ import numpy as np
|
|
| 22 |
import numpy.typing as npt
|
| 23 |
|
| 24 |
|
| 25 |
-
def hz_to_mel(
|
| 26 |
"""
|
| 27 |
-
Convert Hz to mel using the
|
| 28 |
-
|
| 29 |
-
Args:
|
| 30 |
-
freq: Frequencies in Hz.
|
| 31 |
-
|
| 32 |
-
Returns:
|
| 33 |
-
Frequencies in mel.
|
| 34 |
"""
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
"""
|
| 40 |
-
Convert mel to Hz using the
|
| 41 |
-
|
| 42 |
-
Args:
|
| 43 |
-
mels: Values in mel.
|
| 44 |
-
|
| 45 |
-
Returns:
|
| 46 |
-
Frequencies in Hz.
|
| 47 |
"""
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
def librosa_style_mel_filterbank(
|
|
@@ -59,26 +73,15 @@ def librosa_style_mel_filterbank(
|
|
| 59 |
) -> npt.NDArray[np.float32]:
|
| 60 |
"""
|
| 61 |
Build a mel filterbank compatible with librosa.filters.mel using Slaney normalization.
|
| 62 |
-
|
| 63 |
-
Args:
|
| 64 |
-
sr: Sample rate.
|
| 65 |
-
n_fft: FFT size.
|
| 66 |
-
n_mels: Number of mel bins.
|
| 67 |
-
fmin: Minimum frequency in Hz.
|
| 68 |
-
fmax: Maximum frequency in Hz. If None, defaults to sr / 2.
|
| 69 |
-
norm: If "slaney", apply area normalization.
|
| 70 |
-
|
| 71 |
-
Returns:
|
| 72 |
-
Mel filterbank with shape [n_mels, n_fft // 2 + 1].
|
| 73 |
"""
|
| 74 |
if fmax is None:
|
| 75 |
fmax = sr / 2.0
|
| 76 |
|
| 77 |
n_freqs = n_fft // 2 + 1
|
| 78 |
-
|
| 79 |
|
| 80 |
-
m_min = hz_to_mel(
|
| 81 |
-
m_max = hz_to_mel(
|
| 82 |
m_pts = np.linspace(m_min, m_max, n_mels + 2, dtype=np.float64)
|
| 83 |
hz_pts = mel_to_hz(m_pts)
|
| 84 |
|
|
@@ -86,14 +89,11 @@ def librosa_style_mel_filterbank(
|
|
| 86 |
|
| 87 |
for i in range(n_mels):
|
| 88 |
left, center, right = hz_pts[i], hz_pts[i + 1], hz_pts[i + 2]
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
right_slope = (right - freqs) / (right - center + 1e-10)
|
| 92 |
-
|
| 93 |
fb[i] = np.maximum(0.0, np.minimum(left_slope, right_slope))
|
| 94 |
|
| 95 |
if norm == "slaney":
|
| 96 |
-
# Match Slaney-style area normalization used by librosa/torchaudio.
|
| 97 |
enorm = 2.0 / (hz_pts[2:] - hz_pts[:-2])
|
| 98 |
fb *= enorm[:, None]
|
| 99 |
|
|
@@ -107,14 +107,6 @@ def dynamic_range_compression_np(
|
|
| 107 |
) -> npt.NDArray[np.float32]:
|
| 108 |
"""
|
| 109 |
NumPy equivalent of torch.log(torch.clamp(x, min=clip_val) * C).
|
| 110 |
-
|
| 111 |
-
Args:
|
| 112 |
-
x: Input array.
|
| 113 |
-
C: Multiplicative constant.
|
| 114 |
-
clip_val: Minimum allowed value before log.
|
| 115 |
-
|
| 116 |
-
Returns:
|
| 117 |
-
Log-compressed array.
|
| 118 |
"""
|
| 119 |
return np.log(np.clip(x * C, a_min=clip_val, a_max=None)).astype(np.float32)
|
| 120 |
|
|
@@ -122,13 +114,6 @@ def dynamic_range_compression_np(
|
|
| 122 |
def _reflect_pad_1d(x: npt.NDArray[np.float32], pad: int) -> npt.NDArray[np.float32]:
|
| 123 |
"""
|
| 124 |
Reflect-pad a [1, T] waveform along the time axis.
|
| 125 |
-
|
| 126 |
-
Args:
|
| 127 |
-
x: Waveform with shape [1, T].
|
| 128 |
-
pad: Number of samples to pad on each side.
|
| 129 |
-
|
| 130 |
-
Returns:
|
| 131 |
-
Padded waveform with shape [1, T + 2 * pad].
|
| 132 |
"""
|
| 133 |
if pad == 0:
|
| 134 |
return x
|
|
@@ -147,16 +132,6 @@ def _stft_magnitude(
|
|
| 147 |
) -> npt.NDArray[np.float32]:
|
| 148 |
"""
|
| 149 |
Compute magnitude STFT for a single-channel waveform.
|
| 150 |
-
|
| 151 |
-
Args:
|
| 152 |
-
y: Input waveform of shape [1, T].
|
| 153 |
-
n_fft: FFT size.
|
| 154 |
-
hop_size: Hop size between frames.
|
| 155 |
-
win_size: Window size.
|
| 156 |
-
center: Whether to pad the input before framing.
|
| 157 |
-
|
| 158 |
-
Returns:
|
| 159 |
-
Magnitude spectrogram with shape [1, frames, n_fft // 2 + 1].
|
| 160 |
"""
|
| 161 |
if y.ndim != 2 or y.shape[0] != 1:
|
| 162 |
raise ValueError("Expected waveform shape [1, T].")
|
|
@@ -176,7 +151,10 @@ def _stft_magnitude(
|
|
| 176 |
|
| 177 |
frames = x[:, frame_starts[:, None] + frame_offsets[None, :]] # [1, frames, n_fft]
|
| 178 |
|
| 179 |
-
window
|
|
|
|
|
|
|
|
|
|
| 180 |
if n_fft > win_size:
|
| 181 |
pad_left = (n_fft - win_size) // 2
|
| 182 |
pad_right = n_fft - win_size - pad_left
|
|
@@ -204,28 +182,7 @@ def mel_spectrogram_numpy(
|
|
| 204 |
clip_val: float = 1e-5,
|
| 205 |
) -> npt.NDArray[np.float32]:
|
| 206 |
"""
|
| 207 |
-
Compute a mel spectrogram in pure NumPy, matching the torch/torchaudio pipeline.
|
| 208 |
-
|
| 209 |
-
This mirrors:
|
| 210 |
-
- librosa.filters.mel(..., norm="slaney")
|
| 211 |
-
- Hann window STFT
|
| 212 |
-
- power-magnitude spectrogram
|
| 213 |
-
- log compression with clipping
|
| 214 |
-
|
| 215 |
-
Args:
|
| 216 |
-
y: Waveform with shape [1, T].
|
| 217 |
-
n_fft: FFT size.
|
| 218 |
-
num_mels: Number of mel bins.
|
| 219 |
-
sampling_rate: Sampling rate in Hz.
|
| 220 |
-
hop_size: Hop size between frames.
|
| 221 |
-
win_size: Window size.
|
| 222 |
-
fmin: Minimum mel frequency in Hz.
|
| 223 |
-
fmax: Maximum mel frequency in Hz. If None, defaults to sr / 2.
|
| 224 |
-
center: Whether to pad the signal before framing.
|
| 225 |
-
clip_val: Minimum value before log compression.
|
| 226 |
-
|
| 227 |
-
Returns:
|
| 228 |
-
Mel spectrogram with shape [1, num_mels, frames].
|
| 229 |
"""
|
| 230 |
if y.ndim == 1:
|
| 231 |
y = np.expand_dims(y, axis=0)
|
|
@@ -234,11 +191,6 @@ def mel_spectrogram_numpy(
|
|
| 234 |
elif y.ndim > 2:
|
| 235 |
raise ValueError("Expected waveform ndim <= 2.")
|
| 236 |
|
| 237 |
-
if np.min(y) < -1.0:
|
| 238 |
-
pass
|
| 239 |
-
if np.max(y) > 1.0:
|
| 240 |
-
pass
|
| 241 |
-
|
| 242 |
mel_basis = librosa_style_mel_filterbank(
|
| 243 |
sr=sampling_rate,
|
| 244 |
n_fft=n_fft,
|
|
@@ -248,8 +200,15 @@ def mel_spectrogram_numpy(
|
|
| 248 |
norm="slaney",
|
| 249 |
) # [num_mels, n_fft//2 + 1]
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
spec = _stft_magnitude(
|
| 252 |
-
|
| 253 |
n_fft=n_fft,
|
| 254 |
hop_size=hop_size,
|
| 255 |
win_size=win_size,
|
|
@@ -257,7 +216,6 @@ def mel_spectrogram_numpy(
|
|
| 257 |
) # [1, frames, freq]
|
| 258 |
|
| 259 |
mel_spec = np.matmul(mel_basis[None, :, :], np.transpose(spec, (0, 2, 1)))
|
| 260 |
-
mel_spec = np.transpose(mel_spec, (0, 1, 2)) # [1, num_mels, frames]
|
| 261 |
-
|
| 262 |
mel_spec = np.log(np.clip(mel_spec, a_min=clip_val, a_max=None)).astype(np.float32)
|
|
|
|
| 263 |
return mel_spec.transpose(0, 2, 1) # B x T x n_mels
|
|
|
|
| 22 |
import numpy.typing as npt
|
| 23 |
|
| 24 |
|
| 25 |
+
def hz_to_mel(frequencies: npt.NDArray[np.float64] | float) -> npt.NDArray[np.float64] | float:
|
| 26 |
"""
|
| 27 |
+
Convert Hz to mel using the Slaney formula (matching librosa.hz_to_mel).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
+
frequencies = np.asanyarray(frequencies, dtype=float)
|
| 30 |
+
f_min = 0.0
|
| 31 |
+
f_sp = 200.0 / 3
|
| 32 |
+
mels = (frequencies - f_min) / f_sp
|
| 33 |
+
min_log_hz = 1000.0
|
| 34 |
+
min_log_mel = (min_log_hz - f_min) / f_sp
|
| 35 |
+
logstep = np.log(6.4) / 27.0
|
| 36 |
+
|
| 37 |
+
if frequencies.ndim > 0:
|
| 38 |
+
log_mask = frequencies >= min_log_hz
|
| 39 |
+
mels[log_mask] = min_log_mel + np.log(frequencies[log_mask] / min_log_hz) / logstep
|
| 40 |
+
elif frequencies >= min_log_hz:
|
| 41 |
+
mels = min_log_mel + np.log(frequencies / min_log_hz) / logstep
|
| 42 |
+
return mels
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def mel_to_hz(mels: npt.NDArray[np.float64] | float) -> npt.NDArray[np.float64] | float:
|
| 46 |
"""
|
| 47 |
+
Convert mel to Hz using the Slaney formula (matching librosa.mel_to_hz).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
"""
|
| 49 |
+
mels = np.asanyarray(mels, dtype=float)
|
| 50 |
+
f_min = 0.0
|
| 51 |
+
f_sp = 200.0 / 3
|
| 52 |
+
freqs = f_min + f_sp * mels
|
| 53 |
+
min_log_hz = 1000.0
|
| 54 |
+
min_log_mel = (min_log_hz - f_min) / f_sp
|
| 55 |
+
logstep = np.log(6.4) / 27.0
|
| 56 |
+
|
| 57 |
+
if mels.ndim > 0:
|
| 58 |
+
log_mask = mels >= min_log_mel
|
| 59 |
+
freqs[log_mask] = min_log_hz * np.exp(logstep * (mels[log_mask] - min_log_mel))
|
| 60 |
+
elif mels >= min_log_mel:
|
| 61 |
+
freqs = min_log_hz * np.exp(logstep * (mels - min_log_mel))
|
| 62 |
+
return freqs
|
| 63 |
|
| 64 |
|
| 65 |
def librosa_style_mel_filterbank(
|
|
|
|
| 73 |
) -> npt.NDArray[np.float32]:
|
| 74 |
"""
|
| 75 |
Build a mel filterbank compatible with librosa.filters.mel using Slaney normalization.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
if fmax is None:
|
| 78 |
fmax = sr / 2.0
|
| 79 |
|
| 80 |
n_freqs = n_fft // 2 + 1
|
| 81 |
+
fft_freqs = np.linspace(0.0, sr / 2.0, n_freqs, dtype=np.float64)
|
| 82 |
|
| 83 |
+
m_min = hz_to_mel(fmin)
|
| 84 |
+
m_max = hz_to_mel(fmax)
|
| 85 |
m_pts = np.linspace(m_min, m_max, n_mels + 2, dtype=np.float64)
|
| 86 |
hz_pts = mel_to_hz(m_pts)
|
| 87 |
|
|
|
|
| 89 |
|
| 90 |
for i in range(n_mels):
|
| 91 |
left, center, right = hz_pts[i], hz_pts[i + 1], hz_pts[i + 2]
|
| 92 |
+
left_slope = (fft_freqs - left) / (center - left + 1e-10)
|
| 93 |
+
right_slope = (right - fft_freqs) / (right - center + 1e-10)
|
|
|
|
|
|
|
| 94 |
fb[i] = np.maximum(0.0, np.minimum(left_slope, right_slope))
|
| 95 |
|
| 96 |
if norm == "slaney":
|
|
|
|
| 97 |
enorm = 2.0 / (hz_pts[2:] - hz_pts[:-2])
|
| 98 |
fb *= enorm[:, None]
|
| 99 |
|
|
|
|
| 107 |
) -> npt.NDArray[np.float32]:
|
| 108 |
"""
|
| 109 |
NumPy equivalent of torch.log(torch.clamp(x, min=clip_val) * C).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
"""
|
| 111 |
return np.log(np.clip(x * C, a_min=clip_val, a_max=None)).astype(np.float32)
|
| 112 |
|
|
|
|
| 114 |
def _reflect_pad_1d(x: npt.NDArray[np.float32], pad: int) -> npt.NDArray[np.float32]:
|
| 115 |
"""
|
| 116 |
Reflect-pad a [1, T] waveform along the time axis.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
"""
|
| 118 |
if pad == 0:
|
| 119 |
return x
|
|
|
|
| 132 |
) -> npt.NDArray[np.float32]:
|
| 133 |
"""
|
| 134 |
Compute magnitude STFT for a single-channel waveform.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
"""
|
| 136 |
if y.ndim != 2 or y.shape[0] != 1:
|
| 137 |
raise ValueError("Expected waveform shape [1, T].")
|
|
|
|
| 151 |
|
| 152 |
frames = x[:, frame_starts[:, None] + frame_offsets[None, :]] # [1, frames, n_fft]
|
| 153 |
|
| 154 |
+
# Periodic Hann window matching torch.hann_window(win_size, periodic=True)
|
| 155 |
+
n = np.arange(win_size, dtype=np.float32)
|
| 156 |
+
window = 0.5 * (1.0 - np.cos(2.0 * np.pi * n / win_size))
|
| 157 |
+
|
| 158 |
if n_fft > win_size:
|
| 159 |
pad_left = (n_fft - win_size) // 2
|
| 160 |
pad_right = n_fft - win_size - pad_left
|
|
|
|
| 182 |
clip_val: float = 1e-5,
|
| 183 |
) -> npt.NDArray[np.float32]:
|
| 184 |
"""
|
| 185 |
+
Compute a mel spectrogram in pure NumPy, matching the torch/torchaudio pipeline exactly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
"""
|
| 187 |
if y.ndim == 1:
|
| 188 |
y = np.expand_dims(y, axis=0)
|
|
|
|
| 191 |
elif y.ndim > 2:
|
| 192 |
raise ValueError("Expected waveform ndim <= 2.")
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
mel_basis = librosa_style_mel_filterbank(
|
| 195 |
sr=sampling_rate,
|
| 196 |
n_fft=n_fft,
|
|
|
|
| 200 |
norm="slaney",
|
| 201 |
) # [num_mels, n_fft//2 + 1]
|
| 202 |
|
| 203 |
+
# Apply padding if center is False, matching the original torch implementation
|
| 204 |
+
if not center:
|
| 205 |
+
padding = (n_fft - hop_size) // 2
|
| 206 |
+
y_padded = _reflect_pad_1d(y, padding)
|
| 207 |
+
else:
|
| 208 |
+
y_padded = y
|
| 209 |
+
|
| 210 |
spec = _stft_magnitude(
|
| 211 |
+
y_padded,
|
| 212 |
n_fft=n_fft,
|
| 213 |
hop_size=hop_size,
|
| 214 |
win_size=win_size,
|
|
|
|
| 216 |
) # [1, frames, freq]
|
| 217 |
|
| 218 |
mel_spec = np.matmul(mel_basis[None, :, :], np.transpose(spec, (0, 2, 1)))
|
|
|
|
|
|
|
| 219 |
mel_spec = np.log(np.clip(mel_spec, a_min=clip_val, a_max=None)).astype(np.float32)
|
| 220 |
+
|
| 221 |
return mel_spec.transpose(0, 2, 1) # B x T x n_mels
|