Aluode/PerceptionLabPortable
0
1# Copyright 2023 The HuggingFace Inc. team and the librosa & torchaudio authors.2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14"""15Audio processing functions to extract features from audio waveforms. This code is pure numpy to support all frameworks16and remove unnecessary dependencies.17"""18 19import base6420import importlib21import io22import os23import warnings24from collections.abc import Sequence25from io import BytesIO26from typing import TYPE_CHECKING, Any, Optional, Union27 28 29if TYPE_CHECKING:30 import torch31import numpy as np32import requests33from packaging import version34 35from .utils import (36 is_librosa_available,37 is_numpy_array,38 is_soundfile_available,39 is_torch_tensor,40 is_torchcodec_available,41 requires_backends,42)43 44 45if is_soundfile_available():46 import soundfile as sf47 48if is_librosa_available():49 import librosa50 51 # TODO: @eustlb, we actually don't need librosa but soxr is installed with librosa52 import soxr53 54if is_torchcodec_available():55 TORCHCODEC_VERSION = version.parse(importlib.metadata.version("torchcodec"))56 57AudioInput = Union[np.ndarray, "torch.Tensor", Sequence[np.ndarray], Sequence["torch.Tensor"]]58 59 60def load_audio(audio: Union[str, np.ndarray], sampling_rate=16000, timeout=None) -> np.ndarray:61 """62 Loads `audio` to an np.ndarray object.63 64 Args:65 audio (`str` or `np.ndarray`):66 The audio to be loaded to the numpy array format.67 sampling_rate (`int`, *optional*, defaults to 16000):68 The sampling rate to be used when loading the audio. It should be same as the69 sampling rate the model you will be using further was trained with.70 timeout (`float`, *optional*):71 The timeout value in seconds for the URL request.72 73 Returns:74 `np.ndarray`: A numpy array representing the audio.75 """76 if isinstance(audio, str):77 # Try to load with `torchcodec` but do not enforce users to install it. If not found78 # fallback to `librosa`. If using an audio-only model, most probably `torchcodec` won't be79 # needed. Do not raise any errors if not installed or versions do not match80 if is_torchcodec_available() and TORCHCODEC_VERSION >= version.parse("0.3.0"):81 audio = load_audio_torchcodec(audio, sampling_rate=sampling_rate)82 else:83 audio = load_audio_librosa(audio, sampling_rate=sampling_rate, timeout=timeout)84 elif not isinstance(audio, np.ndarray):85 raise TypeError(86 "Incorrect format used for `audio`. Should be an url linking to an audio, a local path, or numpy array."87 )88 return audio89 90 91def load_audio_torchcodec(audio: Union[str, np.ndarray], sampling_rate=16000) -> np.ndarray:92 """93 Loads `audio` to an np.ndarray object using `torchcodec`.94 95 Args:96 audio (`str` or `np.ndarray`):97 The audio to be loaded to the numpy array format.98 sampling_rate (`int`, *optional*, defaults to 16000):99 The sampling rate to be used when loading the audio. It should be same as the100 sampling rate the model you will be using further was trained with.101 102 Returns:103 `np.ndarray`: A numpy array representing the audio.104 """105 # Lazy import so that issues in torchcodec compatibility don't crash the whole library106 requires_backends(load_audio_torchcodec, ["torchcodec"])107 from torchcodec.decoders import AudioDecoder108 109 # Set `num_channels` to `1` which is what most models expects and the default in librosa110 decoder = AudioDecoder(audio, sample_rate=sampling_rate, num_channels=1)111 audio = decoder.get_all_samples().data[0].numpy() # NOTE: feature extractors don't accept torch tensors112 return audio113 114 115def load_audio_librosa(audio: Union[str, np.ndarray], sampling_rate=16000, timeout=None) -> np.ndarray:116 """117 Loads `audio` to an np.ndarray object using `librosa`.118 119 Args:120 audio (`str` or `np.ndarray`):121 The audio to be loaded to the numpy array format.122 sampling_rate (`int`, *optional*, defaults to 16000):123 The sampling rate to be used when loading the audio. It should be same as the124 sampling rate the model you will be using further was trained with.125 timeout (`float`, *optional*):126 The timeout value in seconds for the URL request.127 128 Returns:129 `np.ndarray`: A numpy array representing the audio.130 """131 requires_backends(load_audio_librosa, ["librosa"])132 133 # Load audio from URL (e.g https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen2-Audio/audio/translate_to_chinese.wav)134 if audio.startswith("http://") or audio.startswith("https://"):135 audio = librosa.load(BytesIO(requests.get(audio, timeout=timeout).content), sr=sampling_rate)[0]136 elif os.path.isfile(audio):137 audio = librosa.load(audio, sr=sampling_rate)[0]138 return audio139 140 141def load_audio_as(142 audio: str,143 return_format: str,144 timeout: Optional[int] = None,145 force_mono: bool = False,146 sampling_rate: Optional[int] = None,147) -> Union[str, dict[str, Any], io.BytesIO, None]:148 """149 Load audio from either a local file path or URL and return in specified format.150 151 Args:152 audio (`str`): Either a local file path or a URL to an audio file153 return_format (`str`): Format to return the audio in:154 - "base64": Base64 encoded string155 - "dict": Dictionary with data and format156 - "buffer": BytesIO object157 timeout (`int`, *optional*): Timeout for URL requests in seconds158 force_mono (`bool`): Whether to convert stereo audio to mono159 sampling_rate (`int`, *optional*): If provided, the audio will be resampled to the specified sampling rate.160 161 Returns:162 `Union[str, Dict[str, Any], io.BytesIO, None]`:163 - `str`: Base64 encoded audio data (if return_format="base64")164 - `dict`: Dictionary with 'data' (base64 encoded audio data) and 'format' keys (if return_format="dict")165 - `io.BytesIO`: BytesIO object containing audio data (if return_format="buffer")166 """167 # TODO: @eustlb, we actually don't need librosa but soxr is installed with librosa168 requires_backends(load_audio_as, ["librosa"])169 170 if return_format not in ["base64", "dict", "buffer"]:171 raise ValueError(f"Invalid return_format: {return_format}. Must be 'base64', 'dict', or 'buffer'")172 173 try:174 # Load audio bytes from URL or file175 audio_bytes = None176 if audio.startswith(("http://", "https://")):177 response = requests.get(audio, timeout=timeout)178 response.raise_for_status()179 audio_bytes = response.content180 elif os.path.isfile(audio):181 with open(audio, "rb") as audio_file:182 audio_bytes = audio_file.read()183 else:184 raise ValueError(f"File not found: {audio}")185 186 # Process audio data187 with io.BytesIO(audio_bytes) as audio_file:188 with sf.SoundFile(audio_file) as f:189 audio_array = f.read(dtype="float32")190 original_sr = f.samplerate191 audio_format = f.format192 if sampling_rate is not None and sampling_rate != original_sr:193 # Resample audio to target sampling rate194 audio_array = soxr.resample(audio_array, original_sr, sampling_rate, quality="HQ")195 else:196 sampling_rate = original_sr197 198 # Convert to mono if needed199 if force_mono and audio_array.ndim != 1:200 audio_array = audio_array.mean(axis=1)201 202 buffer = io.BytesIO()203 sf.write(buffer, audio_array, sampling_rate, format=audio_format.upper())204 buffer.seek(0)205 206 if return_format == "buffer":207 return buffer208 elif return_format == "base64":209 return base64.b64encode(buffer.read()).decode("utf-8")210 elif return_format == "dict":211 return {212 "data": base64.b64encode(buffer.read()).decode("utf-8"),213 "format": audio_format.lower(),214 }215 216 except Exception as e:217 raise ValueError(f"Error loading audio: {e}")218 219 220def is_valid_audio(audio):221 return is_numpy_array(audio) or is_torch_tensor(audio)222 223 224def is_valid_list_of_audio(audio):225 return audio and all(is_valid_audio(audio_i) for audio_i in audio)226 227 228def make_list_of_audio(229 audio: Union[list[AudioInput], AudioInput],230) -> AudioInput:231 """232 Ensure that the output is a list of audio.233 Args:234 audio (`Union[list[AudioInput], AudioInput]`):235 The input audio.236 Returns:237 list: A list of audio.238 """239 # If it's a list of audios, it's already in the right format240 if isinstance(audio, (list, tuple)) and is_valid_list_of_audio(audio):241 return audio242 243 # If it's a single audio, convert it to a list of244 if is_valid_audio(audio):245 return [audio]246 247 raise ValueError("Invalid input type. Must be a single audio or a list of audio")248 249 250def hertz_to_mel(freq: Union[float, np.ndarray], mel_scale: str = "htk") -> Union[float, np.ndarray]:251 """252 Convert frequency from hertz to mels.253 254 Args:255 freq (`float` or `np.ndarray`):256 The frequency, or multiple frequencies, in hertz (Hz).257 mel_scale (`str`, *optional*, defaults to `"htk"`):258 The mel frequency scale to use, `"htk"`, `"kaldi"` or `"slaney"`.259 260 Returns:261 `float` or `np.ndarray`: The frequencies on the mel scale.262 """263 264 if mel_scale not in ["slaney", "htk", "kaldi"]:265 raise ValueError('mel_scale should be one of "htk", "slaney" or "kaldi".')266 267 if mel_scale == "htk":268 return 2595.0 * np.log10(1.0 + (freq / 700.0))269 elif mel_scale == "kaldi":270 return 1127.0 * np.log(1.0 + (freq / 700.0))271 272 min_log_hertz = 1000.0273 min_log_mel = 15.0274 logstep = 27.0 / np.log(6.4)275 mels = 3.0 * freq / 200.0276 277 if isinstance(freq, np.ndarray):278 log_region = freq >= min_log_hertz279 mels[log_region] = min_log_mel + np.log(freq[log_region] / min_log_hertz) * logstep280 elif freq >= min_log_hertz:281 mels = min_log_mel + np.log(freq / min_log_hertz) * logstep282 283 return mels284 285 286def mel_to_hertz(mels: Union[float, np.ndarray], mel_scale: str = "htk") -> Union[float, np.ndarray]:287 """288 Convert frequency from mels to hertz.289 290 Args:291 mels (`float` or `np.ndarray`):292 The frequency, or multiple frequencies, in mels.293 mel_scale (`str`, *optional*, `"htk"`):294 The mel frequency scale to use, `"htk"`, `"kaldi"` or `"slaney"`.295 296 Returns:297 `float` or `np.ndarray`: The frequencies in hertz.298 """299 300 if mel_scale not in ["slaney", "htk", "kaldi"]:301 raise ValueError('mel_scale should be one of "htk", "slaney" or "kaldi".')302 303 if mel_scale == "htk":304 return 700.0 * (np.power(10, mels / 2595.0) - 1.0)305 elif mel_scale == "kaldi":306 return 700.0 * (np.exp(mels / 1127.0) - 1.0)307 308 min_log_hertz = 1000.0309 min_log_mel = 15.0310 logstep = np.log(6.4) / 27.0311 freq = 200.0 * mels / 3.0312 313 if isinstance(mels, np.ndarray):314 log_region = mels >= min_log_mel315 freq[log_region] = min_log_hertz * np.exp(logstep * (mels[log_region] - min_log_mel))316 elif mels >= min_log_mel:317 freq = min_log_hertz * np.exp(logstep * (mels - min_log_mel))318 319 return freq320 321 322def hertz_to_octave(freq: Union[float, np.ndarray], tuning: float = 0.0, bins_per_octave: int = 12):323 """324 Convert frequency from hertz to fractional octave numbers.325 Adapted from *librosa*.326 327 Args:328 freq (`float` or `np.ndarray`):329 The frequency, or multiple frequencies, in hertz (Hz).330 tuning (`float`, defaults to `0.`):331 Tuning deviation from the Stuttgart pitch (A440) in (fractional) bins per octave.332 bins_per_octave (`int`, defaults to `12`):333 Number of bins per octave.334 335 Returns:336 `float` or `np.ndarray`: The frequencies on the octave scale.337 """338 stuttgart_pitch = 440.0 * 2.0 ** (tuning / bins_per_octave)339 octave = np.log2(freq / (float(stuttgart_pitch) / 16))340 return octave341 342 343def _create_triangular_filter_bank(fft_freqs: np.ndarray, filter_freqs: np.ndarray) -> np.ndarray:344 """345 Creates a triangular filter bank.346 347 Adapted from *torchaudio* and *librosa*.348 349 Args:350 fft_freqs (`np.ndarray` of shape `(num_frequency_bins,)`):351 Discrete frequencies of the FFT bins in Hz.352 filter_freqs (`np.ndarray` of shape `(num_mel_filters,)`):353 Center frequencies of the triangular filters to create, in Hz.354 355 Returns:356 `np.ndarray` of shape `(num_frequency_bins, num_mel_filters)`357 """358 filter_diff = np.diff(filter_freqs)359 slopes = np.expand_dims(filter_freqs, 0) - np.expand_dims(fft_freqs, 1)360 down_slopes = -slopes[:, :-2] / filter_diff[:-1]361 up_slopes = slopes[:, 2:] / filter_diff[1:]362 return np.maximum(np.zeros(1), np.minimum(down_slopes, up_slopes))363 364 365def chroma_filter_bank(366 num_frequency_bins: int,367 num_chroma: int,368 sampling_rate: int,369 tuning: float = 0.0,370 power: Optional[float] = 2.0,371 weighting_parameters: Optional[tuple[float, float]] = (5.0, 2.0),372 start_at_c_chroma: bool = True,373):374 """375 Creates a chroma filter bank, i.e a linear transformation to project spectrogram bins onto chroma bins.376 377 Adapted from *librosa*.378 379 Args:380 num_frequency_bins (`int`):381 Number of frequencies used to compute the spectrogram (should be the same as in `stft`).382 num_chroma (`int`):383 Number of chroma bins (i.e pitch classes).384 sampling_rate (`float`):385 Sample rate of the audio waveform.386 tuning (`float`):387 Tuning deviation from A440 in fractions of a chroma bin.388 power (`float`, *optional*, defaults to 2.0):389 If 12.0, normalizes each column with their L2 norm. If 1.0, normalizes each column with their L1 norm.390 weighting_parameters (`tuple[float, float]`, *optional*, defaults to `(5., 2.)`):391 If specified, apply a Gaussian weighting parameterized by the first element of the tuple being the center and392 the second element being the Gaussian half-width.393 start_at_c_chroma (`bool`, *optional*, defaults to `True`):394 If True, the filter bank will start at the 'C' pitch class. Otherwise, it will start at 'A'.395 Returns:396 `np.ndarray` of shape `(num_frequency_bins, num_chroma)`397 """398 # Get the FFT bins, not counting the DC component399 frequencies = np.linspace(0, sampling_rate, num_frequency_bins, endpoint=False)[1:]400 401 freq_bins = num_chroma * hertz_to_octave(frequencies, tuning=tuning, bins_per_octave=num_chroma)402 403 # make up a value for the 0 Hz bin = 1.5 octaves below bin 1404 # (so chroma is 50% rotated from bin 1, and bin width is broad)405 freq_bins = np.concatenate(([freq_bins[0] - 1.5 * num_chroma], freq_bins))406 407 bins_width = np.concatenate((np.maximum(freq_bins[1:] - freq_bins[:-1], 1.0), [1]))408 409 chroma_filters = np.subtract.outer(freq_bins, np.arange(0, num_chroma, dtype="d")).T410 411 num_chroma2 = np.round(float(num_chroma) / 2)412 413 # Project into range -num_chroma/2 .. num_chroma/2414 # add on fixed offset of 10*num_chroma to ensure all values passed to415 # rem are positive416 chroma_filters = np.remainder(chroma_filters + num_chroma2 + 10 * num_chroma, num_chroma) - num_chroma2417 418 # Gaussian bumps - 2*D to make them narrower419 chroma_filters = np.exp(-0.5 * (2 * chroma_filters / np.tile(bins_width, (num_chroma, 1))) ** 2)420 421 # normalize each column422 if power is not None:423 chroma_filters = chroma_filters / np.sum(chroma_filters**power, axis=0, keepdims=True) ** (1.0 / power)424 425 # Maybe apply scaling for fft bins426 if weighting_parameters is not None:427 center, half_width = weighting_parameters428 chroma_filters *= np.tile(429 np.exp(-0.5 * (((freq_bins / num_chroma - center) / half_width) ** 2)),430 (num_chroma, 1),431 )432 433 if start_at_c_chroma:434 chroma_filters = np.roll(chroma_filters, -3 * (num_chroma // 12), axis=0)435 436 # remove aliasing columns, copy to ensure row-contiguity437 return np.ascontiguousarray(chroma_filters[:, : int(1 + num_frequency_bins / 2)])438 439 440def mel_filter_bank(441 num_frequency_bins: int,442 num_mel_filters: int,443 min_frequency: float,444 max_frequency: float,445 sampling_rate: int,446 norm: Optional[str] = None,447 mel_scale: str = "htk",448 triangularize_in_mel_space: bool = False,449) -> np.ndarray:450 """451 Creates a frequency bin conversion matrix used to obtain a mel spectrogram. This is called a *mel filter bank*, and452 various implementation exist, which differ in the number of filters, the shape of the filters, the way the filters453 are spaced, the bandwidth of the filters, and the manner in which the spectrum is warped. The goal of these454 features is to approximate the non-linear human perception of the variation in pitch with respect to the frequency.455 456 Different banks of mel filters were introduced in the literature. The following variations are supported:457 458 - MFCC FB-20: introduced in 1980 by Davis and Mermelstein, it assumes a sampling frequency of 10 kHz and a speech459 bandwidth of `[0, 4600]` Hz.460 - MFCC FB-24 HTK: from the Cambridge HMM Toolkit (HTK) (1995) uses a filter bank of 24 filters for a speech461 bandwidth of `[0, 8000]` Hz. This assumes sampling rate ≥ 16 kHz.462 - MFCC FB-40: from the Auditory Toolbox for MATLAB written by Slaney in 1998, assumes a sampling rate of 16 kHz and463 speech bandwidth of `[133, 6854]` Hz. This version also includes area normalization.464 - HFCC-E FB-29 (Human Factor Cepstral Coefficients) of Skowronski and Harris (2004), assumes a sampling rate of465 12.5 kHz and speech bandwidth of `[0, 6250]` Hz.466 467 This code is adapted from *torchaudio* and *librosa*. Note that the default parameters of torchaudio's468 `melscale_fbanks` implement the `"htk"` filters while librosa uses the `"slaney"` implementation.469 470 Args:471 num_frequency_bins (`int`):472 Number of frequency bins (should be the same as `n_fft // 2 + 1` where `n_fft` is the size of the Fourier Transform used to compute the spectrogram).473 num_mel_filters (`int`):474 Number of mel filters to generate.475 min_frequency (`float`):476 Lowest frequency of interest in Hz.477 max_frequency (`float`):478 Highest frequency of interest in Hz. This should not exceed `sampling_rate / 2`.479 sampling_rate (`int`):480 Sample rate of the audio waveform.481 norm (`str`, *optional*):482 If `"slaney"`, divide the triangular mel weights by the width of the mel band (area normalization).483 mel_scale (`str`, *optional*, defaults to `"htk"`):484 The mel frequency scale to use, `"htk"`, `"kaldi"` or `"slaney"`.485 triangularize_in_mel_space (`bool`, *optional*, defaults to `False`):486 If this option is enabled, the triangular filter is applied in mel space rather than frequency space. This487 should be set to `true` in order to get the same results as `torchaudio` when computing mel filters.488 489 Returns:490 `np.ndarray` of shape (`num_frequency_bins`, `num_mel_filters`): Triangular filter bank matrix. This is a491 projection matrix to go from a spectrogram to a mel spectrogram.492 """493 if norm is not None and norm != "slaney":494 raise ValueError('norm must be one of None or "slaney"')495 496 if num_frequency_bins < 2:497 raise ValueError(f"Require num_frequency_bins: {num_frequency_bins} >= 2")498 499 if min_frequency > max_frequency:500 raise ValueError(f"Require min_frequency: {min_frequency} <= max_frequency: {max_frequency}")501 502 # center points of the triangular mel filters503 mel_min = hertz_to_mel(min_frequency, mel_scale=mel_scale)504 mel_max = hertz_to_mel(max_frequency, mel_scale=mel_scale)505 mel_freqs = np.linspace(mel_min, mel_max, num_mel_filters + 2)506 filter_freqs = mel_to_hertz(mel_freqs, mel_scale=mel_scale)507 508 if triangularize_in_mel_space:509 # frequencies of FFT bins in Hz, but filters triangularized in mel space510 fft_bin_width = sampling_rate / ((num_frequency_bins - 1) * 2)511 fft_freqs = hertz_to_mel(fft_bin_width * np.arange(num_frequency_bins), mel_scale=mel_scale)512 filter_freqs = mel_freqs513 else:514 # frequencies of FFT bins in Hz515 fft_freqs = np.linspace(0, sampling_rate // 2, num_frequency_bins)516 517 mel_filters = _create_triangular_filter_bank(fft_freqs, filter_freqs)518 519 if norm is not None and norm == "slaney":520 # Slaney-style mel is scaled to be approx constant energy per channel521 enorm = 2.0 / (filter_freqs[2 : num_mel_filters + 2] - filter_freqs[:num_mel_filters])522 mel_filters *= np.expand_dims(enorm, 0)523 524 if (mel_filters.max(axis=0) == 0.0).any():525 warnings.warn(526 "At least one mel filter has all zero values. "527 f"The value for `num_mel_filters` ({num_mel_filters}) may be set too high. "528 f"Or, the value for `num_frequency_bins` ({num_frequency_bins}) may be set too low."529 )530 531 return mel_filters532 533 534def optimal_fft_length(window_length: int) -> int:535 """536 Finds the best FFT input size for a given `window_length`. This function takes a given window length and, if not537 already a power of two, rounds it up to the next power or two.538 539 The FFT algorithm works fastest when the length of the input is a power of two, which may be larger than the size540 of the window or analysis frame. For example, if the window is 400 samples, using an FFT input size of 512 samples541 is more optimal than an FFT size of 400 samples. Using a larger FFT size does not affect the detected frequencies,542 it simply gives a higher frequency resolution (i.e. the frequency bins are smaller).543 """544 return 2 ** int(np.ceil(np.log2(window_length)))545 546 547def window_function(548 window_length: int,549 name: str = "hann",550 periodic: bool = True,551 frame_length: Optional[int] = None,552 center: bool = True,553) -> np.ndarray:554 """555 Returns an array containing the specified window. This window is intended to be used with `stft`.556 557 The following window types are supported:558 559 - `"boxcar"`: a rectangular window560 - `"hamming"`: the Hamming window561 - `"hann"`: the Hann window562 - `"povey"`: the Povey window563 564 Args:565 window_length (`int`):566 The length of the window in samples.567 name (`str`, *optional*, defaults to `"hann"`):568 The name of the window function.569 periodic (`bool`, *optional*, defaults to `True`):570 Whether the window is periodic or symmetric.571 frame_length (`int`, *optional*):572 The length of the analysis frames in samples. Provide a value for `frame_length` if the window is smaller573 than the frame length, so that it will be zero-padded.574 center (`bool`, *optional*, defaults to `True`):575 Whether to center the window inside the FFT buffer. Only used when `frame_length` is provided.576 577 Returns:578 `np.ndarray` of shape `(window_length,)` or `(frame_length,)` containing the window.579 """580 length = window_length + 1 if periodic else window_length581 582 if name == "boxcar":583 window = np.ones(length)584 elif name in ["hamming", "hamming_window"]:585 window = np.hamming(length)586 elif name in ["hann", "hann_window"]:587 window = np.hanning(length)588 elif name == "povey":589 window = np.power(np.hanning(length), 0.85)590 else:591 raise ValueError(f"Unknown window function '{name}'")592 593 if periodic:594 window = window[:-1]595 596 if frame_length is None:597 return window598 599 if window_length > frame_length:600 raise ValueError(601 f"Length of the window ({window_length}) may not be larger than frame_length ({frame_length})"602 )603 604 padded_window = np.zeros(frame_length)605 offset = (frame_length - window_length) // 2 if center else 0606 padded_window[offset : offset + window_length] = window607 return padded_window608 609 610# TODO This method does not support batching yet as we are mainly focused on inference.611def spectrogram(612 waveform: np.ndarray,613 window: np.ndarray,614 frame_length: int,615 hop_length: int,616 fft_length: Optional[int] = None,617 power: Optional[float] = 1.0,618 center: bool = True,619 pad_mode: str = "reflect",620 onesided: bool = True,621 dither: float = 0.0,622 preemphasis: Optional[float] = None,623 mel_filters: Optional[np.ndarray] = None,624 mel_floor: float = 1e-10,625 log_mel: Optional[str] = None,626 reference: float = 1.0,627 min_value: float = 1e-10,628 db_range: Optional[float] = None,629 remove_dc_offset: bool = False,630 dtype: np.dtype = np.float32,631) -> np.ndarray:632 """633 Calculates a spectrogram over one waveform using the Short-Time Fourier Transform.634 635 This function can create the following kinds of spectrograms:636 637 - amplitude spectrogram (`power = 1.0`)638 - power spectrogram (`power = 2.0`)639 - complex-valued spectrogram (`power = None`)640 - log spectrogram (use `log_mel` argument)641 - mel spectrogram (provide `mel_filters`)642 - log-mel spectrogram (provide `mel_filters` and `log_mel`)643 644 How this works:645 646 1. The input waveform is split into frames of size `frame_length` that are partially overlapping by `frame_length647 - hop_length` samples.648 2. Each frame is multiplied by the window and placed into a buffer of size `fft_length`.649 3. The DFT is taken of each windowed frame.650 4. The results are stacked into a spectrogram.651 652 We make a distinction between the following "blocks" of sample data, each of which may have a different lengths:653 654 - The analysis frame. This is the size of the time slices that the input waveform is split into.655 - The window. Each analysis frame is multiplied by the window to avoid spectral leakage.656 - The FFT input buffer. The length of this determines how many frequency bins are in the spectrogram.657 658 In this implementation, the window is assumed to be zero-padded to have the same size as the analysis frame. A659 padded window can be obtained from `window_function()`. The FFT input buffer may be larger than the analysis frame,660 typically the next power of two.661 662 Note: This function is not optimized for speed yet. It should be mostly compatible with `librosa.stft` and663 `torchaudio.functional.transforms.Spectrogram`, although it is more flexible due to the different ways spectrograms664 can be constructed.665 666 Args:667 waveform (`np.ndarray` of shape `(length,)`):668 The input waveform. This must be a single real-valued, mono waveform.669 window (`np.ndarray` of shape `(frame_length,)`):670 The windowing function to apply, including zero-padding if necessary. The actual window length may be671 shorter than `frame_length`, but we're assuming the array has already been zero-padded.672 frame_length (`int`):673 The length of the analysis frames in samples. With librosa this is always equal to `fft_length` but we also674 allow smaller sizes.675 hop_length (`int`):676 The stride between successive analysis frames in samples.677 fft_length (`int`, *optional*):678 The size of the FFT buffer in samples. This determines how many frequency bins the spectrogram will have.679 For optimal speed, this should be a power of two. If `None`, uses `frame_length`.680 power (`float`, *optional*, defaults to 1.0):681 If 1.0, returns the amplitude spectrogram. If 2.0, returns the power spectrogram. If `None`, returns682 complex numbers.683 center (`bool`, *optional*, defaults to `True`):684 Whether to pad the waveform so that frame `t` is centered around time `t * hop_length`. If `False`, frame685 `t` will start at time `t * hop_length`.686 pad_mode (`str`, *optional*, defaults to `"reflect"`):687 Padding mode used when `center` is `True`. Possible values are: `"constant"` (pad with zeros), `"edge"`688 (pad with edge values), `"reflect"` (pads with mirrored values).689 onesided (`bool`, *optional*, defaults to `True`):690 If True, only computes the positive frequencies and returns a spectrogram containing `fft_length // 2 + 1`691 frequency bins. If False, also computes the negative frequencies and returns `fft_length` frequency bins.692 dither (`float`, *optional*, defaults to 0.0):693 Adds dithering. In other words, adds a small Gaussian noise to each frame.694 E.g. use 4.0 to add dithering with a normal distribution centered695 around 0.0 with standard deviation 4.0, 0.0 means no dithering.696 Dithering has similar effect as `mel_floor`. It reduces the high log_mel_fbank697 values for signals with hard-zero sections, when VAD cutoff is present in the signal.698 preemphasis (`float`, *optional*)699 Coefficient for a low-pass filter that applies pre-emphasis before the DFT.700 mel_filters (`np.ndarray` of shape `(num_freq_bins, num_mel_filters)`, *optional*):701 The mel filter bank. If supplied, applies a this filter bank to create a mel spectrogram.702 mel_floor (`float`, *optional*, defaults to 1e-10):703 Minimum value of mel frequency banks.704 log_mel (`str`, *optional*):705 How to convert the spectrogram to log scale. Possible options are: `None` (don't convert), `"log"` (take706 the natural logarithm) `"log10"` (take the base-10 logarithm), `"dB"` (convert to decibels). Can only be707 used when `power` is not `None`.708 reference (`float`, *optional*, defaults to 1.0):709 Sets the input spectrogram value that corresponds to 0 dB. For example, use `np.max(spectrogram)` to set710 the loudest part to 0 dB. Must be greater than zero.711 min_value (`float`, *optional*, defaults to `1e-10`):712 The spectrogram will be clipped to this minimum value before conversion to decibels, to avoid taking713 `log(0)`. For a power spectrogram, the default of `1e-10` corresponds to a minimum of -100 dB. For an714 amplitude spectrogram, the value `1e-5` corresponds to -100 dB. Must be greater than zero.715 db_range (`float`, *optional*):716 Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the difference between the717 peak value and the smallest value will never be more than 80 dB. Must be greater than zero.718 remove_dc_offset (`bool`, *optional*):719 Subtract mean from waveform on each frame, applied before pre-emphasis. This should be set to `true` in720 order to get the same results as `torchaudio.compliance.kaldi.fbank` when computing mel filters.721 dtype (`np.dtype`, *optional*, defaults to `np.float32`):722 Data type of the spectrogram tensor. If `power` is None, this argument is ignored and the dtype will be723 `np.complex64`.724 725 Returns:726 `nd.array` containing a spectrogram of shape `(num_frequency_bins, length)` for a regular spectrogram or shape727 `(num_mel_filters, length)` for a mel spectrogram.728 """729 window_length = len(window)730 731 if fft_length is None:732 fft_length = frame_length733 734 if frame_length > fft_length:735 raise ValueError(f"frame_length ({frame_length}) may not be larger than fft_length ({fft_length})")736 737 if window_length != frame_length:738 raise ValueError(f"Length of the window ({window_length}) must equal frame_length ({frame_length})")739 740 if hop_length <= 0:741 raise ValueError("hop_length must be greater than zero")742 743 if waveform.ndim != 1:744 raise ValueError(f"Input waveform must have only one dimension, shape is {waveform.shape}")745 746 if np.iscomplexobj(waveform):747 raise ValueError("Complex-valued input waveforms are not currently supported")748 749 if power is None and mel_filters is not None:750 raise ValueError(751 "You have provided `mel_filters` but `power` is `None`. Mel spectrogram computation is not yet supported for complex-valued spectrogram."752 "Specify `power` to fix this issue."753 )754 755 # center pad the waveform756 if center:757 padding = [(int(frame_length // 2), int(frame_length // 2))]758 waveform = np.pad(waveform, padding, mode=pad_mode)759 760 # promote to float64, since np.fft uses float64 internally761 waveform = waveform.astype(np.float64)762 window = window.astype(np.float64)763 764 # split waveform into frames of frame_length size765 num_frames = int(1 + np.floor((waveform.size - frame_length) / hop_length))766 767 num_frequency_bins = (fft_length // 2) + 1 if onesided else fft_length768 spectrogram = np.empty((num_frames, num_frequency_bins), dtype=np.complex64)769 770 # rfft is faster than fft771 fft_func = np.fft.rfft if onesided else np.fft.fft772 buffer = np.zeros(fft_length)773 774 timestep = 0775 for frame_idx in range(num_frames):776 buffer[:frame_length] = waveform[timestep : timestep + frame_length]777 778 if dither != 0.0:779 buffer[:frame_length] += dither * np.random.randn(frame_length)780 781 if remove_dc_offset:782 buffer[:frame_length] = buffer[:frame_length] - buffer[:frame_length].mean()783 784 if preemphasis is not None:785 buffer[1:frame_length] -= preemphasis * buffer[: frame_length - 1]786 buffer[0] *= 1 - preemphasis787 788 buffer[:frame_length] *= window789 790 spectrogram[frame_idx] = fft_func(buffer)791 timestep += hop_length792 793 # note: ** is much faster than np.power794 if power is not None:795 spectrogram = np.abs(spectrogram, dtype=np.float64) ** power796 797 spectrogram = spectrogram.T798 799 if mel_filters is not None:800 spectrogram = np.maximum(mel_floor, np.dot(mel_filters.T, spectrogram))801 802 if power is not None and log_mel is not None:803 if log_mel == "log":804 spectrogram = np.log(spectrogram)805 elif log_mel == "log10":806 spectrogram = np.log10(spectrogram)807 elif log_mel == "dB":808 if power == 1.0:809 spectrogram = amplitude_to_db(spectrogram, reference, min_value, db_range)810 elif power == 2.0:811 spectrogram = power_to_db(spectrogram, reference, min_value, db_range)812 else:813 raise ValueError(f"Cannot use log_mel option '{log_mel}' with power {power}")814 else:815 raise ValueError(f"Unknown log_mel option: {log_mel}")816 817 spectrogram = np.asarray(spectrogram, dtype)818 819 return spectrogram820 821 822def spectrogram_batch(823 waveform_list: list[np.ndarray],824 window: np.ndarray,825 frame_length: int,826 hop_length: int,827 fft_length: Optional[int] = None,828 power: Optional[float] = 1.0,829 center: bool = True,830 pad_mode: str = "reflect",831 onesided: bool = True,832 dither: float = 0.0,833 preemphasis: Optional[float] = None,834 mel_filters: Optional[np.ndarray] = None,835 mel_floor: float = 1e-10,836 log_mel: Optional[str] = None,837 reference: float = 1.0,838 min_value: float = 1e-10,839 db_range: Optional[float] = None,840 remove_dc_offset: bool = False,841 dtype: np.dtype = np.float32,842) -> list[np.ndarray]:843 """844 Calculates spectrograms for a list of waveforms using the Short-Time Fourier Transform, optimized for batch processing.845 This function extends the capabilities of the `spectrogram` function to handle multiple waveforms efficiently by leveraging broadcasting.846 847 It supports generating various types of spectrograms:848 849 - amplitude spectrogram (`power = 1.0`)850 - power spectrogram (`power = 2.0`)851 - complex-valued spectrogram (`power = None`)852 - log spectrogram (use `log_mel` argument)853 - mel spectrogram (provide `mel_filters`)854 - log-mel spectrogram (provide `mel_filters` and `log_mel`)855 856 How this works:857 858 1. The input waveform is split into frames of size `frame_length` that are partially overlapping by `frame_length859 - hop_length` samples.860 2. Each frame is multiplied by the window and placed into a buffer of size `fft_length`.861 3. The DFT is taken of each windowed frame.862 4. The results are stacked into a spectrogram.863 864 We make a distinction between the following "blocks" of sample data, each of which may have a different lengths:865 866 - The analysis frame. This is the size of the time slices that the input waveform is split into.867 - The window. Each analysis frame is multiplied by the window to avoid spectral leakage.868 - The FFT input buffer. The length of this determines how many frequency bins are in the spectrogram.869 870 In this implementation, the window is assumed to be zero-padded to have the same size as the analysis frame. A871 padded window can be obtained from `window_function()`. The FFT input buffer may be larger than the analysis frame,872 typically the next power of two.873 874 Note: This function is designed for efficient batch processing of multiple waveforms but retains compatibility with individual waveform processing methods like `librosa.stft`.875 876 Args:877 waveform_list (`list[np.ndarray]` with arrays of shape `(length,)`):878 The list of input waveforms, each a single-channel (mono) signal.879 window (`np.ndarray` of shape `(frame_length,)`):880 The windowing function to apply, including zero-padding if necessary.881 frame_length (`int`):882 The length of each frame for analysis.883 hop_length (`int`):884 The step size between successive frames.885 fft_length (`int`, *optional*):886 The size of the FFT buffer, defining frequency bin resolution.887 power (`float`, *optional*, defaults to 1.0):888 Determines the type of spectrogram: 1.0 for amplitude, 2.0 for power, None for complex.889 center (`bool`, *optional*, defaults to `True`):890 Whether to center-pad the waveform frames.891 pad_mode (`str`, *optional*, defaults to `"reflect"`):892 The padding strategy when `center` is `True`.893 onesided (`bool`, *optional*, defaults to `True`):894 If True, returns a one-sided spectrogram for real input signals.895 dither (`float`, *optional*, defaults to 0.0):896 Adds dithering. In other words, adds a small Gaussian noise to each frame.897 E.g. use 4.0 to add dithering with a normal distribution centered898 around 0.0 with standard deviation 4.0, 0.0 means no dithering.899 preemphasis (`float`, *optional*):900 Applies a pre-emphasis filter to each frame.901 mel_filters (`np.ndarray`, *optional*):902 Mel filter bank for converting to mel spectrogram.903 mel_floor (`float`, *optional*, defaults to 1e-10):904 Floor value for mel spectrogram to avoid log(0).905 log_mel (`str`, *optional*):906 Specifies log scaling strategy; options are None, "log", "log10", "dB".907 reference (`float`, *optional*, defaults to 1.0):908 Reference value for dB conversion in log_mel.909 min_value (`float`, *optional*, defaults to 1e-10):910 Minimum floor value for log scale conversions.911 db_range (`float`, *optional*):912 Dynamic range for dB scale spectrograms.913 remove_dc_offset (`bool`, *optional*):914 Whether to remove the DC offset from each frame.915 dtype (`np.dtype`, *optional*, defaults to `np.float32`):916 Data type of the output spectrogram.917 918 Returns:919 list[`np.ndarray`]: A list of spectrogram arrays, one for each input waveform.920 """921 window_length = len(window)922 923 if fft_length is None:924 fft_length = frame_length925 926 if frame_length > fft_length:927 raise ValueError(f"frame_length ({frame_length}) may not be larger than fft_length ({fft_length})")928 929 if window_length != frame_length:930 raise ValueError(f"Length of the window ({window_length}) must equal frame_length ({frame_length})")931 932 if hop_length <= 0:933 raise ValueError("hop_length must be greater than zero")934 935 # Check the dimensions of the waveform , and if waveform is complex936 for waveform in waveform_list:937 if waveform.ndim != 1:938 raise ValueError(f"Input waveform must have only one dimension, shape is {waveform.shape}")939 if np.iscomplexobj(waveform):940 raise ValueError("Complex-valued input waveforms are not currently supported")941 # Center pad the waveform942 if center:943 padding = [(int(frame_length // 2), int(frame_length // 2))]944 waveform_list = [945 np.pad(946 waveform,947 padding,948 mode=pad_mode,949 )950 for waveform in waveform_list951 ]952 original_waveform_lengths = [953 len(waveform) for waveform in waveform_list954 ] # these lengths will be used to remove padding later955 956 # Batch pad the waveform957 max_length = max(original_waveform_lengths)958 padded_waveform_batch = np.array(959 [960 np.pad(waveform, (0, max_length - len(waveform)), mode="constant", constant_values=0)961 for waveform in waveform_list962 ],963 dtype=dtype,964 )965 966 # Promote to float64, since np.fft uses float64 internally967 padded_waveform_batch = padded_waveform_batch.astype(np.float64)968 window = window.astype(np.float64)969 970 # Split waveform into frames of frame_length size971 num_frames = int(1 + np.floor((padded_waveform_batch.shape[1] - frame_length) / hop_length))972 # these lengths will be used to remove padding later973 true_num_frames = [int(1 + np.floor((length - frame_length) / hop_length)) for length in original_waveform_lengths]974 num_batches = padded_waveform_batch.shape[0]975 976 num_frequency_bins = (fft_length // 2) + 1 if onesided else fft_length977 spectrogram = np.empty((num_batches, num_frames, num_frequency_bins), dtype=np.complex64)978 979 # rfft is faster than fft980 fft_func = np.fft.rfft if onesided else np.fft.fft981 buffer = np.zeros((num_batches, fft_length))982 983 for frame_idx in range(num_frames):984 timestep = frame_idx * hop_length985 buffer[:, :frame_length] = padded_waveform_batch[:, timestep : timestep + frame_length]986 987 if dither != 0.0:988 buffer[:, :frame_length] += dither * np.random.randn(*buffer[:, :frame_length].shape)989 990 if remove_dc_offset:991 buffer[:, :frame_length] -= buffer[:, :frame_length].mean(axis=1, keepdims=True)992 993 if preemphasis is not None:994 buffer[:, 1:frame_length] -= preemphasis * buffer[:, : frame_length - 1]995 buffer[:, 0] *= 1 - preemphasis996 997 buffer[:, :frame_length] *= window998 999 spectrogram[:, frame_idx] = fft_func(buffer)1000 1001 # Note: ** is much faster than np.power1002 if power is not None:1003 spectrogram = np.abs(spectrogram, dtype=np.float64) ** power1004 1005 # Apply mel filters if provided1006 if mel_filters is not None:1007 result = np.tensordot(spectrogram, mel_filters.T, axes=([2], [1]))1008 spectrogram = np.maximum(mel_floor, result)1009 1010 # Convert to log scale if specified1011 if power is not None and log_mel is not None:1012 if log_mel == "log":1013 spectrogram = np.log(spectrogram)1014 elif log_mel == "log10":1015 spectrogram = np.log10(spectrogram)1016 elif log_mel == "dB":1017 if power == 1.0:1018 spectrogram = amplitude_to_db_batch(spectrogram, reference, min_value, db_range)1019 elif power == 2.0:1020 spectrogram = power_to_db_batch(spectrogram, reference, min_value, db_range)1021 else:1022 raise ValueError(f"Cannot use log_mel option '{log_mel}' with power {power}")1023 else:1024 raise ValueError(f"Unknown log_mel option: {log_mel}")1025 1026 spectrogram = np.asarray(spectrogram, dtype)1027 1028 spectrogram_list = [spectrogram[i, : true_num_frames[i], :].T for i in range(len(true_num_frames))]1029 1030 return spectrogram_list1031 1032 1033def power_to_db(1034 spectrogram: np.ndarray,1035 reference: float = 1.0,1036 min_value: float = 1e-10,1037 db_range: Optional[float] = None,1038) -> np.ndarray:1039 """1040 Converts a power spectrogram to the decibel scale. This computes `10 * log10(spectrogram / reference)`, using basic1041 logarithm properties for numerical stability.1042 1043 The motivation behind applying the log function on the (mel) spectrogram is that humans do not hear loudness on a1044 linear scale. Generally to double the perceived volume of a sound we need to put 8 times as much energy into it.1045 This means that large variations in energy may not sound all that different if the sound is loud to begin with.1046 This compression operation makes the (mel) spectrogram features match more closely what humans actually hear.1047 1048 Based on the implementation of `librosa.power_to_db`.1049 1050 Args:1051 spectrogram (`np.ndarray`):1052 The input power (mel) spectrogram. Note that a power spectrogram has the amplitudes squared!1053 reference (`float`, *optional*, defaults to 1.0):1054 Sets the input spectrogram value that corresponds to 0 dB. For example, use `np.max(spectrogram)` to set1055 the loudest part to 0 dB. Must be greater than zero.1056 min_value (`float`, *optional*, defaults to `1e-10`):1057 The spectrogram will be clipped to this minimum value before conversion to decibels, to avoid taking1058 `log(0)`. The default of `1e-10` corresponds to a minimum of -100 dB. Must be greater than zero.1059 db_range (`float`, *optional*):1060 Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the difference between the1061 peak value and the smallest value will never be more than 80 dB. Must be greater than zero.1062 1063 Returns:1064 `np.ndarray`: the spectrogram in decibels1065 """1066 if reference <= 0.0:1067 raise ValueError("reference must be greater than zero")1068 if min_value <= 0.0:1069 raise ValueError("min_value must be greater than zero")1070 1071 reference = max(min_value, reference)1072 1073 spectrogram = np.clip(spectrogram, a_min=min_value, a_max=None)1074 spectrogram = 10.0 * (np.log10(spectrogram) - np.log10(reference))1075 1076 if db_range is not None:1077 if db_range <= 0.0:1078 raise ValueError("db_range must be greater than zero")1079 spectrogram = np.clip(spectrogram, a_min=spectrogram.max() - db_range, a_max=None)1080 1081 return spectrogram1082 1083 1084def power_to_db_batch(1085 spectrogram: np.ndarray,1086 reference: float = 1.0,1087 min_value: float = 1e-10,1088 db_range: Optional[float] = None,1089) -> np.ndarray:1090 """1091 Converts a batch of power spectrograms to the decibel scale. This computes `10 * log10(spectrogram / reference)`,1092 using basic logarithm properties for numerical stability.1093 1094 This function supports batch processing, where each item in the batch is an individual power (mel) spectrogram.1095 1096 Args:1097 spectrogram (`np.ndarray`):1098 The input batch of power (mel) spectrograms. Expected shape is (batch_size, *spectrogram_shape).1099 Note that a power spectrogram has the amplitudes squared!1100 reference (`float`, *optional*, defaults to 1.0):1101 Sets the input spectrogram value that corresponds to 0 dB. For example, use `np.max(spectrogram)` to set1102 the loudest part to 0 dB. Must be greater than zero.1103 min_value (`float`, *optional*, defaults to `1e-10`):1104 The spectrogram will be clipped to this minimum value before conversion to decibels, to avoid taking1105 `log(0)`. The default of `1e-10` corresponds to a minimum of -100 dB. Must be greater than zero.1106 db_range (`float`, *optional*):1107 Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the difference between the1108 peak value and the smallest value will never be more than 80 dB. Must be greater than zero.1109 1110 Returns:1111 `np.ndarray`: the batch of spectrograms in decibels1112 """1113 if reference <= 0.0:1114 raise ValueError("reference must be greater than zero")1115 if min_value <= 0.0:1116 raise ValueError("min_value must be greater than zero")1117 1118 reference = max(min_value, reference)1119 1120 spectrogram = np.clip(spectrogram, a_min=min_value, a_max=None)1121 spectrogram = 10.0 * (np.log10(spectrogram) - np.log10(reference))1122 1123 if db_range is not None:1124 if db_range <= 0.0:1125 raise ValueError("db_range must be greater than zero")1126 # Apply db_range clipping per batch item1127 max_values = spectrogram.max(axis=(1, 2), keepdims=True)1128 spectrogram = np.clip(spectrogram, a_min=max_values - db_range, a_max=None)1129 1130 return spectrogram1131 1132 1133def amplitude_to_db(1134 spectrogram: np.ndarray,1135 reference: float = 1.0,1136 min_value: float = 1e-5,1137 db_range: Optional[float] = None,1138) -> np.ndarray:1139 """1140 Converts an amplitude spectrogram to the decibel scale. This computes `20 * log10(spectrogram / reference)`, using1141 basic logarithm properties for numerical stability.1142 1143 The motivation behind applying the log function on the (mel) spectrogram is that humans do not hear loudness on a1144 linear scale. Generally to double the perceived volume of a sound we need to put 8 times as much energy into it.1145 This means that large variations in energy may not sound all that different if the sound is loud to begin with.1146 This compression operation makes the (mel) spectrogram features match more closely what humans actually hear.1147 1148 Args:1149 spectrogram (`np.ndarray`):1150 The input amplitude (mel) spectrogram.1151 reference (`float`, *optional*, defaults to 1.0):1152 Sets the input spectrogram value that corresponds to 0 dB. For example, use `np.max(spectrogram)` to set1153 the loudest part to 0 dB. Must be greater than zero.1154 min_value (`float`, *optional*, defaults to `1e-5`):1155 The spectrogram will be clipped to this minimum value before conversion to decibels, to avoid taking1156 `log(0)`. The default of `1e-5` corresponds to a minimum of -100 dB. Must be greater than zero.1157 db_range (`float`, *optional*):1158 Sets the maximum dynamic range in decibels. For example, if `db_range = 80`, the difference between the1159 peak value and the smallest value will never be more than 80 dB. Must be greater than zero.1160 1161 Returns:1162 `np.ndarray`: the spectrogram in decibels1163 """1164 if reference <= 0.0:1165 raise ValueError("reference must be greater than zero")1166 if min_value <= 0.0:1167 raise ValueError("min_value must be greater than zero")1168 1169 reference = max(min_value, reference)1170 1171 spectrogram = np.clip(spectrogram, a_min=min_value, a_max=None)1172 spectrogram = 20.0 * (np.log10(spectrogram) - np.log10(reference))1173 1174 if db_range is not None:1175 if db_range <= 0.0:1176 raise ValueError("db_range must be greater than zero")1177 spectrogram = np.clip(spectrogram, a_min=spectrogram.max() - db_range, a_max=None)1178 1179 return spectrogram1180 1181 1182def amplitude_to_db_batch(1183 spectrogram: np.ndarray, reference: float = 1.0, min_value: float = 1e-5, db_range: Optional[float] = None1184) -> np.ndarray:1185 """1186 Converts a batch of amplitude spectrograms to the decibel scale. This computes `20 * log10(spectrogram / reference)`,1187 using basic logarithm properties for numerical stability.1188 1189 The function supports batch processing, where each item in the batch is an individual amplitude (mel) spectrogram.1190 1191 Args:1192 spectrogram (`np.ndarray`):1193 The input batch of amplitude (mel) spectrograms. Expected shape is (batch_size, *spectrogram_shape).1194 reference (`float`, *optional*, defaults to 1.0):1195 Sets the input spectrogram value that corresponds to 0 dB. For example, use `np.max(spectrogram)` to set1196 the loudest part to 0 dB. Must be greater than zero.1197 min_value (`float`, *optional*, defaults to `1e-5`):1198 The spectrogram will be clipped to this minimum value before conversion to decibels, to avoid taking1199 `log(0)`. The default of `1e-5` corresponds to a minimum of -100 dB. Must be greater than zero.1200 db_range (`float`, *optional*):