# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Modified from librosa(https://github.com/librosa/librosa) from __future__ import annotations import math from typing import TYPE_CHECKING, Literal, TypeVar import paddle from paddle import Tensor from paddle.base.framework import Variable from paddle.pir import Value if TYPE_CHECKING: _TensorOrFloat = TypeVar("_TensorOrFloat", Tensor, float) def hz_to_mel(freq: _TensorOrFloat, htk: bool = False) -> _TensorOrFloat: """Convert Hz to Mels. Args: freq (Union[Tensor, float]): The input tensor with arbitrary shape. htk (bool, optional): Use htk scaling. Defaults to False. Returns: Union[Tensor, float]: Frequency in mels. Examples: .. code-block:: pycon >>> import paddle >>> val = 3.0 >>> htk_flag = True >>> mel_paddle_tensor = paddle.audio.functional.hz_to_mel(paddle.to_tensor(val), htk_flag) """ if htk: if isinstance(freq, (Tensor, Variable, Value)): return 2595.0 * paddle.log10(1.0 + freq / 700.0) else: return 2595.0 * math.log10(1.0 + freq / 700.0) # Fill in the linear part f_min = 0.0 f_sp = 200.0 / 3 mels = (freq - f_min) / f_sp # Fill in the log-scale part min_log_hz = 1000.0 # beginning of log region (Hz) min_log_mel = (min_log_hz - f_min) / f_sp # same (Mels) logstep = math.log(6.4) / 27.0 # step size for log region if isinstance(freq, (Tensor, Variable, Value)): target = ( min_log_mel + paddle.log(freq / min_log_hz + 1e-10) / logstep ) # prevent nan with 1e-10 mask = (freq > min_log_hz).astype(freq.dtype) mels = target * mask + mels * ( 1 - mask ) # will replace by masked_fill OP in future else: if freq >= min_log_hz: mels = min_log_mel + math.log(freq / min_log_hz + 1e-10) / logstep return mels def mel_to_hz(mel: _TensorOrFloat, htk: bool = False) -> _TensorOrFloat: """Convert mel bin numbers to frequencies. Args: mel (Union[float, Tensor]): The mel frequency represented as a tensor with arbitrary shape. htk (bool, optional): Use htk scaling. Defaults to False. Returns: Union[float, Tensor]: Frequencies in Hz. Examples: .. code-block:: pycon >>> import paddle >>> val = 3.0 >>> htk_flag = True >>> mel_paddle_tensor = paddle.audio.functional.mel_to_hz(paddle.to_tensor(val), htk_flag) """ if htk: return 700.0 * (10.0 ** (mel / 2595.0) - 1.0) f_min = 0.0 f_sp = 200.0 / 3 freqs = f_min + f_sp * mel # And now the nonlinear scale min_log_hz = 1000.0 # beginning of log region (Hz) min_log_mel = (min_log_hz - f_min) / f_sp # same (Mels) logstep = math.log(6.4) / 27.0 # step size for log region if isinstance(mel, (Tensor, Variable, Value)): target = min_log_hz * paddle.exp(logstep * (mel - min_log_mel)) mask = (mel > min_log_mel).astype(mel.dtype) freqs = target * mask + freqs * ( 1 - mask ) # will replace by masked_fill OP in future else: if mel >= min_log_mel: freqs = min_log_hz * math.exp(logstep * (mel - min_log_mel)) return freqs def mel_frequencies( n_mels: int = 64, f_min: float = 0.0, f_max: float = 11025.0, htk: bool = False, dtype: str = 'float32', ) -> Tensor: """Compute mel frequencies. Args: n_mels (int, optional): Number of mel bins. Defaults to 64. f_min (float, optional): Minimum frequency in Hz. Defaults to 0.0. fmax (float, optional): Maximum frequency in Hz. Defaults to 11025.0. htk (bool, optional): Use htk scaling. Defaults to False. dtype (str, optional): The data type of the return frequencies. Defaults to 'float32'. Returns: Tensor: Tensor of n_mels frequencies in Hz with shape `(n_mels,)`. Examples: .. code-block:: pycon >>> import paddle >>> n_mels = 64 >>> f_min = 0.5 >>> f_max = 10000 >>> htk_flag = True >>> paddle_mel_freq = paddle.audio.functional.mel_frequencies(n_mels, f_min, f_max, htk_flag, 'float64') """ # 'Center freqs' of mel bands - uniformly spaced between limits min_mel = hz_to_mel(f_min, htk=htk) max_mel = hz_to_mel(f_max, htk=htk) mels = paddle.linspace(min_mel, max_mel, n_mels, dtype=dtype) freqs = mel_to_hz(mels, htk=htk) return freqs def fft_frequencies(sr: int, n_fft: int, dtype: str = 'float32') -> Tensor: """Compute fourier frequencies. Args: sr (int): Sample rate. n_fft (int): Number of fft bins. dtype (str, optional): The data type of the return frequencies. Defaults to 'float32'. Returns: Tensor: FFT frequencies in Hz with shape `(n_fft//2 + 1,)`. Examples: .. code-block:: pycon >>> import paddle >>> sr = 16000 >>> n_fft = 128 >>> fft_freq = paddle.audio.functional.fft_frequencies(sr, n_fft) """ return paddle.linspace(0, float(sr) / 2, int(1 + n_fft // 2), dtype=dtype) def compute_fbank_matrix( sr: int, n_fft: int, n_mels: int = 64, f_min: float = 0.0, f_max: float | None = None, htk: bool = False, norm: Literal['slaney'] | float = 'slaney', dtype: str = 'float32', ) -> Tensor: """Compute fbank matrix. Args: sr (int): Sample rate. n_fft (int): Number of fft bins. n_mels (int, optional): Number of mel bins. Defaults to 64. f_min (float, optional): Minimum frequency in Hz. Defaults to 0.0. f_max (Optional[float], optional): Maximum frequency in Hz. Defaults to None. htk (bool, optional): Use htk scaling. Defaults to False. norm (Union[str, float], optional): Type of normalization. Defaults to 'slaney'. dtype (str, optional): The data type of the return matrix. Defaults to 'float32'. Returns: Tensor: Mel transform matrix with shape `(n_mels, n_fft//2 + 1)`. Examples: .. code-block:: pycon >>> import paddle >>> sr = 23 >>> n_fft = 51 >>> fbank = paddle.audio.functional.compute_fbank_matrix(sr, n_fft) """ if f_max is None: f_max = float(sr) / 2 # Initialize the weights weights = paddle.zeros((n_mels, int(1 + n_fft // 2)), dtype=dtype) # Center freqs of each FFT bin fftfreqs = fft_frequencies(sr=sr, n_fft=n_fft, dtype=dtype) # 'Center freqs' of mel bands - uniformly spaced between limits mel_f = mel_frequencies( n_mels + 2, f_min=f_min, f_max=f_max, htk=htk, dtype=dtype ) fdiff = mel_f[1:] - mel_f[:-1] # np.diff(mel_f) ramps = mel_f.unsqueeze(1) - fftfreqs.unsqueeze(0) # ramps = np.subtract.outer(mel_f, fftfreqs) for i in range(n_mels): # lower and upper slopes for all bins lower = -ramps[i] / fdiff[i] upper = ramps[i + 2] / fdiff[i + 1] # .. then intersect them with each other and zero weights[i] = paddle.maximum( paddle.zeros_like(lower), paddle.minimum(lower, upper) ) # Slaney-style mel is scaled to be approx constant energy per channel if norm == 'slaney': enorm = 2.0 / (mel_f[2 : n_mels + 2] - mel_f[:n_mels]) weights *= enorm.unsqueeze(1) elif isinstance(norm, (int, float)): weights = paddle.nn.functional.normalize(weights, p=norm, axis=-1) return weights def power_to_db( spect: Tensor, ref_value: float = 1.0, amin: float = 1e-10, top_db: float | None = 80.0, ) -> Tensor: """Convert a power spectrogram (amplitude squared) to decibel (dB) units. The function computes the scaling `10 * log10(x / ref)` in a numerically stable way. Args: spect (Tensor): STFT power spectrogram. ref_value (float, optional): The reference value. If smaller than 1.0, the db level of the signal will be pulled up accordingly. Otherwise, the db level is pushed down. Defaults to 1.0. amin (float, optional): Minimum threshold. Defaults to 1e-10. top_db (Optional[float], optional): Threshold the output at `top_db` below the peak. Defaults to None. Returns: Tensor: Power spectrogram in db scale. Examples: .. code-block:: pycon >>> import paddle >>> val = 3.0 >>> decibel_paddle = paddle.audio.functional.power_to_db(paddle.to_tensor(val)) """ if amin <= 0: raise Exception("amin must be strictly positive") if ref_value <= 0: raise Exception("ref_value must be strictly positive") ones = paddle.ones_like(spect) log_spec = 10.0 * paddle.log10(paddle.maximum(ones * amin, spect)) log_spec -= 10.0 * math.log10(max(ref_value, amin)) if top_db is not None: if top_db < 0: raise Exception("top_db must be non-negative") log_spec = paddle.maximum(log_spec, ones * (log_spec.max() - top_db)) return log_spec def create_dct( n_mfcc: int, n_mels: int, norm: Literal['ortho'] | None = 'ortho', dtype: str = 'float32', ) -> Tensor: """Create a discrete cosine transform(DCT) matrix. Args: n_mfcc (int): Number of mel frequency cepstral coefficients. n_mels (int): Number of mel filterbanks. norm (Optional[str], optional): Normalization type. Defaults to 'ortho'. dtype (str, optional): The data type of the return matrix. Defaults to 'float32'. Returns: Tensor: The DCT matrix with shape `(n_mels, n_mfcc)`. Examples: .. code-block:: pycon >>> import paddle >>> n_mfcc = 23 >>> n_mels = 257 >>> dct = paddle.audio.functional.create_dct(n_mfcc, n_mels) """ n = paddle.arange(n_mels, dtype=dtype) k = paddle.arange(n_mfcc, dtype=dtype).unsqueeze(1) dct = paddle.cos( math.pi / float(n_mels) * (n + 0.5) * k ) # size (n_mfcc, n_mels) if norm is None: dct *= 2.0 else: assert norm == "ortho" dct[0] *= 1.0 / math.sqrt(2.0) dct *= math.sqrt(2.0 / float(n_mels)) return dct.T def _get_sinc_resample_kernel( orig_freq: int, new_freq: int, gcd: int, lowpass_filter_width: int = 6, rolloff: float = 0.99, resampling_method: Literal[ "sinc_interp_hann", "sinc_interp_kaiser" ] = "sinc_interp_hann", beta: float | None = None, dtype: paddle.dtype | None = None, ): """ Generate the sinc interpolation kernel for resampling. This internal function computes the resampling kernel based on the sinc interpolation formula with windowing. The kernel is used by _apply_sinc_resample_kernel to perform the actual resampling. Args: orig_freq (int): Original sampling frequency. new_freq (int): Target sampling frequency. gcd (int): Greatest common divisor of orig_freq and new_freq. lowpass_filter_width (int, optional): Controls the sharpness of the filter, larger value means sharper but less efficient. Default: 6. rolloff (float, optional): Roll-off frequency as a fraction of the Nyquist. Lower values reduce anti-aliasing but also attenuate high frequencies. Default: 0.99. resampling_method (str, optional): Window method for filter design. Options: ["sinc_interp_hann", "sinc_interp_kaiser"]. Default: "sinc_interp_hann". beta (float, optional): Shape parameter for Kaiser window. Required only when resampling_method="sinc_interp_kaiser". Default: None. dtype (paddle.dtype, optional): Data type for kernel computation. If None, uses float64 for computation and converts to float32 for output. Default: None. Returns: tuple: (kernel, width) - kernel (Tensor): Resampling kernel of shape (1, 1, kernel_width) - width (int): Half-width of the filter in terms of input samples Raises: Exception: If frequencies are not integers. ValueError: If resampling_method is invalid or lowpass_filter_width <= 0. """ if not (int(orig_freq) == orig_freq and int(new_freq) == new_freq): raise ValueError( "Frequencies must be of integer type to ensure quality resampling computation. " "To work around this, manually convert both frequencies to integer values " "that maintain their resampling rate ratio before passing them into the function. " "Example: To downsample a 44100 hz waveform by a factor of 8, use " "`orig_freq=8` and `new_freq=1` instead of `orig_freq=44100` and `new_freq=5512.5`. " ) if resampling_method not in ["sinc_interp_hann", "sinc_interp_kaiser"]: raise ValueError(f"Invalid resampling method: {resampling_method}") orig_freq = int(orig_freq) // gcd new_freq = int(new_freq) // gcd if lowpass_filter_width <= 0: raise ValueError("Low pass filter width should be positive.") base_freq = min(orig_freq, new_freq) # Perform antialiasing filtering by removing the highest frequencies. base_freq *= rolloff # Calculate filter width based on lowpass_filter_width and frequency ratio width = math.ceil(lowpass_filter_width * orig_freq / base_freq) idx_dtype = dtype if dtype is not None else paddle.float64 idx = ( paddle.arange(-width, width + orig_freq, dtype=idx_dtype)[None, None] / orig_freq ) t = ( paddle.arange(0, -new_freq, -1, dtype=dtype)[:, None, None] / new_freq + idx ) t *= base_freq t = t.clip_(-lowpass_filter_width, lowpass_filter_width) # we do not use built-in paddle windows here as we need to evaluate the window # at specific positions, not over a regular grid. if resampling_method == "sinc_interp_hann": window = paddle.cos(t * math.pi / lowpass_filter_width / 2) ** 2 else: # sinc_interp_kaiser if beta is None: beta = 14.769656459379492 beta_tensor = paddle.to_tensor(float(beta)) window = paddle.i0( beta_tensor * paddle.sqrt(1 - (t / lowpass_filter_width) ** 2), ) / paddle.i0(beta_tensor) t *= math.pi scale = base_freq / orig_freq kernels = paddle.where( t == 0, paddle.to_tensor(1.0).cast(t.dtype), t.sin() / t ) kernels *= window * scale if dtype is None: # pragma: no cover kernels = kernels.cast(paddle.float32) return kernels, width def _apply_sinc_resample_kernel( waveform: Tensor, orig_freq: int, new_freq: int, gcd: int, kernel: Tensor, width: int, ): """ Apply sinc interpolation resampling using precomputed kernel. This internal function performs the actual resampling operation using the kernel generated by _get_sinc_resample_kernel. It handles batch processing and ensures correct output length. Args: waveform (Tensor): Input waveform of shape (..., time). Must be floating point. orig_freq (int): Original sampling frequency. new_freq (int): Target sampling frequency. gcd (int): Greatest common divisor of orig_freq and new_freq. kernel (Tensor): Resampling kernel from _get_sinc_resample_kernel. width (int): Half-width of the filter from _get_sinc_resample_kernel. Returns: Tensor: Resampled waveform of shape (..., new_time). """ orig_freq = int(orig_freq) // gcd new_freq = int(new_freq) // gcd # pack batch shape = waveform.shape waveform = waveform.reshape([-1, shape[-1]]) num_wavs, length = waveform.shape waveform = paddle.nn.functional.pad(waveform, (width, width + orig_freq)) resampled = paddle.nn.functional.conv1d( waveform[:, None], kernel, stride=orig_freq ) resampled = resampled.transpose([0, 2, 1]).reshape((num_wavs, -1)) target_length = paddle.ceil( paddle.to_tensor(new_freq * length / orig_freq) ).astype(paddle.int64) resampled = resampled[..., :target_length] # unpack batch resampled = resampled.reshape(shape[:-1] + resampled.shape[-1:]) return resampled def resample( waveform: Tensor, orig_freq: int, new_freq: int, lowpass_filter_width: int = 6, rolloff: float = 0.99, resampling_method: Literal[ "sinc_interp_hann", "sinc_interp_kaiser" ] = "sinc_interp_hann", beta: float | None = None, ) -> Tensor: """ Resample the waveform from orig_freq to new_freq using bandlimited interpolation. This function implements resampling through sinc interpolation with windowing. It first computes a resampling kernel based on the specified parameters, then applies it to the input waveform using convolution. The algorithm handles both upsampling and downsampling while minimizing aliasing artifacts. Args: waveform (Tensor): The input signal of dimension (..., time). Must be floating point type (float32 or float64). orig_freq (int): The original frequency of the signal. Must be positive. new_freq (int): The desired target frequency. Must be positive. lowpass_filter_width (int, optional): Controls the sharpness of the filter. Larger values give sharper filtering but are less efficient. Default: 6. rolloff (float, optional): The roll-off frequency of the filter as a fraction of the Nyquist frequency. Lower values reduce anti-aliasing but also attenuate some high frequencies. Default: 0.99. resampling_method (str, optional): The windowing method to use for filter design. Options: "sinc_interp_hann" (Hann window) or "sinc_interp_kaiser" (Kaiser window). Default: "sinc_interp_hann". beta (float, optional): Shape parameter for the Kaiser window. Required only when resampling_method="sinc_interp_kaiser". If not provided for Kaiser, a default value of 14.769656459379492 is used. Default: None. Returns: Tensor: The waveform resampled to new_freq, with dimension (..., new_time). Raises: ValueError: If orig_freq or new_freq are not positive. Exception: If frequencies are not integers (see note below). TypeError: If waveform is not floating point. Note: - orig_freq and new_freq must be integers. For non-integer frequencies, convert them to integers while maintaining the ratio. - For repeated resampling with same parameters, use :class:`paddle.audio.transforms.Resample` for better efficiency. - Uses windowed sinc interpolation for high-quality audio resampling. - This function does not support ONNX export now. Examples: .. code-block:: pycon >>> import paddle >>> from paddle.audio.functional import resample >>> # Create a sample waveform (1 channel, 1000 samples at 16000 Hz) >>> waveform = paddle.randn([1, 1000]) >>> # Downsample from 16000 Hz to 8000 Hz >>> resampled = resample(waveform, 16000, 8000) >>> print(resampled.shape) paddle.Size([1, 500]) >>> # Upsample from 16000 Hz to 48000 Hz with custom filter width >>> resampled = resample(waveform, 16000, 48000, lowpass_filter_width=12) >>> print(resampled.shape) paddle.Size([1, 3000]) >>> # Use Kaiser window resampling >>> resampled = resample(waveform, 16000, 8000, resampling_method="sinc_interp_kaiser", beta=12.0) >>> print(resampled.shape) paddle.Size([1, 500]) >>> # Batch processing: multiple waveforms >>> batch_waveforms = paddle.randn([4, 1, 1000]) # [batch, channels, time] >>> resampled_batch = resample(batch_waveforms, 16000, 8000) >>> print(resampled_batch.shape) paddle.Size([4, 1, 500]) """ if orig_freq <= 0.0 or new_freq <= 0.0: raise ValueError( "Original frequency and desired frequency should be positive integers" ) if not waveform.is_floating_point(): raise TypeError( f"Expected floating point type for waveform tensor, but received {waveform.dtype}." ) if orig_freq == new_freq: return waveform gcd = math.gcd(int(orig_freq), int(new_freq)) kernel, width = _get_sinc_resample_kernel( orig_freq, new_freq, gcd, lowpass_filter_width, rolloff, resampling_method, beta, waveform.dtype, ) resampled = _apply_sinc_resample_kernel( waveform, orig_freq, new_freq, gcd, kernel, width ) return resampled