pltobing commited on
Commit
4915823
·
1 Parent(s): 8370970

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.

Files changed (1) hide show
  1. src/utils/audio_utils.py +53 -95
src/utils/audio_utils.py CHANGED
@@ -22,30 +22,44 @@ import numpy as np
22
  import numpy.typing as npt
23
 
24
 
25
- def hz_to_mel(freq: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
26
  """
27
- Convert Hz to mel using the HTK formula.
28
-
29
- Args:
30
- freq: Frequencies in Hz.
31
-
32
- Returns:
33
- Frequencies in mel.
34
  """
35
- return 2595.0 * np.log10(1.0 + freq / 700.0)
36
-
37
-
38
- def mel_to_hz(mels: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  """
40
- Convert mel to Hz using the HTK formula.
41
-
42
- Args:
43
- mels: Values in mel.
44
-
45
- Returns:
46
- Frequencies in Hz.
47
  """
48
- return 700.0 * (10.0 ** (mels / 2595.0) - 1.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- freqs = np.linspace(0.0, sr / 2.0, n_freqs, dtype=np.float64)
79
 
80
- m_min = hz_to_mel(np.array([fmin], dtype=np.float64))[0]
81
- m_max = hz_to_mel(np.array([fmax], dtype=np.float64))[0]
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
- left_slope = (freqs - left) / (center - left + 1e-10)
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 = np.hanning(win_size).astype(np.float32)
 
 
 
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
- y,
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