850 lines
24 KiB
Python
850 lines
24 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Effects
|
|
=======
|
|
|
|
Harmonic-percussive source separation
|
|
-------------------------------------
|
|
.. autosummary::
|
|
:toctree: generated/
|
|
|
|
hpss
|
|
harmonic
|
|
percussive
|
|
|
|
Time and frequency
|
|
------------------
|
|
.. autosummary::
|
|
:toctree: generated/
|
|
|
|
time_stretch
|
|
pitch_shift
|
|
|
|
Miscellaneous
|
|
-------------
|
|
.. autosummary::
|
|
:toctree: generated/
|
|
|
|
remix
|
|
trim
|
|
split
|
|
preemphasis
|
|
deemphasis
|
|
"""
|
|
|
|
import numpy as np
|
|
import scipy.signal
|
|
|
|
from . import core
|
|
from . import decompose
|
|
from . import feature
|
|
from . import util
|
|
from .util.exceptions import ParameterError
|
|
from typing import Any, Callable, Iterable, Optional, Tuple, Union, overload
|
|
from typing_extensions import Literal
|
|
from numpy.typing import ArrayLike
|
|
|
|
__all__ = [
|
|
"hpss",
|
|
"harmonic",
|
|
"percussive",
|
|
"time_stretch",
|
|
"pitch_shift",
|
|
"remix",
|
|
"trim",
|
|
"split",
|
|
]
|
|
|
|
|
|
def hpss(y: np.ndarray, **kwargs: Any) -> Tuple[np.ndarray, np.ndarray]:
|
|
"""Decompose an audio time series into harmonic and percussive components.
|
|
|
|
This function automates the STFT->HPSS->ISTFT pipeline, and ensures that
|
|
the output waveforms have equal length to the input waveform ``y``.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
audio time series. Multi-channel is supported.
|
|
**kwargs : additional keyword arguments.
|
|
See `librosa.decompose.hpss` for details.
|
|
|
|
Returns
|
|
-------
|
|
y_harmonic : np.ndarray [shape=(..., n)]
|
|
audio time series of the harmonic elements
|
|
y_percussive : np.ndarray [shape=(..., n)]
|
|
audio time series of the percussive elements
|
|
|
|
See Also
|
|
--------
|
|
harmonic : Extract only the harmonic component
|
|
percussive : Extract only the percussive component
|
|
librosa.decompose.hpss : HPSS on spectrograms
|
|
|
|
Examples
|
|
--------
|
|
>>> # Extract harmonic and percussive components
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
>>> y_harmonic, y_percussive = librosa.effects.hpss(y)
|
|
|
|
>>> # Get a more isolated percussive component by widening its margin
|
|
>>> y_harmonic, y_percussive = librosa.effects.hpss(y, margin=(1.0,5.0))
|
|
"""
|
|
# Compute the STFT matrix
|
|
stft = core.stft(y)
|
|
|
|
# Decompose into harmonic and percussives
|
|
stft_harm, stft_perc = decompose.hpss(stft, **kwargs)
|
|
|
|
# Invert the STFTs. Adjust length to match the input.
|
|
y_harm = core.istft(stft_harm, dtype=y.dtype, length=y.shape[-1])
|
|
y_perc = core.istft(stft_perc, dtype=y.dtype, length=y.shape[-1])
|
|
|
|
return y_harm, y_perc
|
|
|
|
|
|
def harmonic(y: np.ndarray, **kwargs: Any) -> np.ndarray:
|
|
"""Extract harmonic elements from an audio time-series.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
audio time series. Multi-channel is supported.
|
|
**kwargs : additional keyword arguments.
|
|
See `librosa.decompose.hpss` for details.
|
|
|
|
Returns
|
|
-------
|
|
y_harmonic : np.ndarray [shape=(..., n)]
|
|
audio time series of just the harmonic portion
|
|
|
|
See Also
|
|
--------
|
|
hpss : Separate harmonic and percussive components
|
|
percussive : Extract only the percussive component
|
|
librosa.decompose.hpss : HPSS for spectrograms
|
|
|
|
Examples
|
|
--------
|
|
>>> # Extract harmonic component
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
>>> y_harmonic = librosa.effects.harmonic(y)
|
|
|
|
>>> # Use a margin > 1.0 for greater harmonic separation
|
|
>>> y_harmonic = librosa.effects.harmonic(y, margin=3.0)
|
|
"""
|
|
# Compute the STFT matrix
|
|
stft = core.stft(y)
|
|
|
|
# Remove percussives
|
|
stft_harm = decompose.hpss(stft, **kwargs)[0]
|
|
|
|
# Invert the STFTs
|
|
y_harm = core.istft(stft_harm, dtype=y.dtype, length=y.shape[-1])
|
|
|
|
return y_harm
|
|
|
|
|
|
def percussive(y: np.ndarray, **kwargs: Any) -> np.ndarray:
|
|
"""Extract percussive elements from an audio time-series.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
audio time series. Multi-channel is supported.
|
|
**kwargs : additional keyword arguments.
|
|
See `librosa.decompose.hpss` for details.
|
|
|
|
Returns
|
|
-------
|
|
y_percussive : np.ndarray [shape=(..., n)]
|
|
audio time series of just the percussive portion
|
|
|
|
See Also
|
|
--------
|
|
hpss : Separate harmonic and percussive components
|
|
harmonic : Extract only the harmonic component
|
|
librosa.decompose.hpss : HPSS for spectrograms
|
|
|
|
Examples
|
|
--------
|
|
>>> # Extract percussive component
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
>>> y_percussive = librosa.effects.percussive(y)
|
|
|
|
>>> # Use a margin > 1.0 for greater percussive separation
|
|
>>> y_percussive = librosa.effects.percussive(y, margin=3.0)
|
|
"""
|
|
# Compute the STFT matrix
|
|
stft = core.stft(y)
|
|
|
|
# Remove harmonics
|
|
stft_perc = decompose.hpss(stft, **kwargs)[1]
|
|
|
|
# Invert the STFT
|
|
y_perc = core.istft(stft_perc, dtype=y.dtype, length=y.shape[-1])
|
|
|
|
return y_perc
|
|
|
|
|
|
def time_stretch(y: np.ndarray, *, rate: float, **kwargs: Any) -> np.ndarray:
|
|
"""Time-stretch an audio series by a fixed rate.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
audio time series. Multi-channel is supported.
|
|
rate : float > 0 [scalar]
|
|
Stretch factor. If ``rate > 1``, then the signal is sped up.
|
|
If ``rate < 1``, then the signal is slowed down.
|
|
**kwargs : additional keyword arguments.
|
|
See `librosa.decompose.stft` for details.
|
|
|
|
Returns
|
|
-------
|
|
y_stretch : np.ndarray [shape=(..., round(n/rate))]
|
|
audio time series stretched by the specified rate
|
|
|
|
See Also
|
|
--------
|
|
pitch_shift :
|
|
pitch shifting
|
|
librosa.phase_vocoder :
|
|
spectrogram phase vocoder
|
|
pyrubberband.pyrb.time_stretch :
|
|
high-quality time stretching using RubberBand
|
|
|
|
Examples
|
|
--------
|
|
Compress to be twice as fast
|
|
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
>>> y_fast = librosa.effects.time_stretch(y, rate=2.0)
|
|
|
|
Or half the original speed
|
|
|
|
>>> y_slow = librosa.effects.time_stretch(y, rate=0.5)
|
|
"""
|
|
if rate <= 0:
|
|
raise ParameterError("rate must be a positive number")
|
|
|
|
# Construct the short-term Fourier transform (STFT)
|
|
stft = core.stft(y, **kwargs)
|
|
|
|
# Stretch by phase vocoding
|
|
stft_stretch = core.phase_vocoder(
|
|
stft,
|
|
rate=rate,
|
|
hop_length=kwargs.get("hop_length", None),
|
|
n_fft=kwargs.get("n_fft", None),
|
|
)
|
|
|
|
# Predict the length of y_stretch
|
|
len_stretch = int(round(y.shape[-1] / rate))
|
|
|
|
# Invert the STFT
|
|
y_stretch = core.istft(stft_stretch, dtype=y.dtype, length=len_stretch, **kwargs)
|
|
|
|
return y_stretch
|
|
|
|
|
|
def pitch_shift(
|
|
y: np.ndarray,
|
|
*,
|
|
sr: float,
|
|
n_steps: float,
|
|
bins_per_octave: int = 12,
|
|
res_type: str = "soxr_hq",
|
|
scale: bool = False,
|
|
**kwargs: Any,
|
|
) -> np.ndarray:
|
|
"""Shift the pitch of a waveform by ``n_steps`` steps.
|
|
|
|
A step is equal to a semitone if ``bins_per_octave`` is set to 12.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
audio time series. Multi-channel is supported.
|
|
|
|
sr : number > 0 [scalar]
|
|
audio sampling rate of ``y``
|
|
|
|
n_steps : float [scalar]
|
|
how many (fractional) steps to shift ``y``
|
|
|
|
bins_per_octave : int > 0 [scalar]
|
|
how many steps per octave
|
|
|
|
res_type : string
|
|
Resample type. By default, 'soxr_hq' is used.
|
|
|
|
See `librosa.resample` for more information.
|
|
|
|
scale : bool
|
|
Scale the resampled signal so that ``y`` and ``y_hat`` have approximately
|
|
equal total energy.
|
|
|
|
**kwargs : additional keyword arguments.
|
|
See `librosa.decompose.stft` for details.
|
|
|
|
Returns
|
|
-------
|
|
y_shift : np.ndarray [shape=(..., n)]
|
|
The pitch-shifted audio time-series
|
|
|
|
See Also
|
|
--------
|
|
time_stretch :
|
|
time stretching
|
|
librosa.phase_vocoder :
|
|
spectrogram phase vocoder
|
|
pyrubberband.pyrb.pitch_shift :
|
|
high-quality pitch shifting using RubberBand
|
|
|
|
Examples
|
|
--------
|
|
Shift up by a major third (four steps if ``bins_per_octave`` is 12)
|
|
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
>>> y_third = librosa.effects.pitch_shift(y, sr=sr, n_steps=4)
|
|
|
|
Shift down by a tritone (six steps if ``bins_per_octave`` is 12)
|
|
|
|
>>> y_tritone = librosa.effects.pitch_shift(y, sr=sr, n_steps=-6)
|
|
|
|
Shift up by 3 quarter-tones
|
|
|
|
>>> y_three_qt = librosa.effects.pitch_shift(y, sr=sr, n_steps=3,
|
|
... bins_per_octave=24)
|
|
"""
|
|
if not util.is_positive_int(bins_per_octave):
|
|
raise ParameterError(
|
|
f"bins_per_octave={bins_per_octave} must be a positive integer."
|
|
)
|
|
|
|
rate = 2.0 ** (-float(n_steps) / bins_per_octave)
|
|
|
|
# Stretch in time, then resample
|
|
y_shift = core.resample(
|
|
time_stretch(y, rate=rate, **kwargs),
|
|
orig_sr=float(sr) / rate,
|
|
target_sr=sr,
|
|
res_type=res_type,
|
|
scale=scale,
|
|
)
|
|
|
|
# Crop to the same dimension as the input
|
|
return util.fix_length(y_shift, size=y.shape[-1])
|
|
|
|
|
|
def remix(
|
|
y: np.ndarray, intervals: Iterable[Tuple[int, int]], *, align_zeros: bool = True
|
|
) -> np.ndarray:
|
|
"""Remix an audio signal by re-ordering time intervals.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., t)]
|
|
Audio time series. Multi-channel is supported.
|
|
intervals : iterable of tuples (start, end)
|
|
An iterable (list-like or generator) where the ``i``th item
|
|
``intervals[i]`` indicates the start and end (in samples)
|
|
of a slice of ``y``.
|
|
align_zeros : boolean
|
|
If ``True``, interval boundaries are mapped to the closest
|
|
zero-crossing in ``y``. If ``y`` is stereo, zero-crossings
|
|
are computed after converting to mono.
|
|
|
|
Returns
|
|
-------
|
|
y_remix : np.ndarray [shape=(..., d)]
|
|
``y`` remixed in the order specified by ``intervals``
|
|
|
|
Examples
|
|
--------
|
|
Load in the example track and reverse the beats
|
|
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
|
|
Compute beats
|
|
|
|
>>> _, beat_frames = librosa.beat.beat_track(y=y, sr=sr,
|
|
... hop_length=512)
|
|
|
|
Convert from frames to sample indices
|
|
|
|
>>> beat_samples = librosa.frames_to_samples(beat_frames)
|
|
|
|
Generate intervals from consecutive events
|
|
|
|
>>> intervals = librosa.util.frame(beat_samples, frame_length=2,
|
|
... hop_length=1).T
|
|
|
|
Reverse the beat intervals
|
|
|
|
>>> y_out = librosa.effects.remix(y, intervals[::-1])
|
|
"""
|
|
y_out = []
|
|
|
|
if align_zeros:
|
|
y_mono = core.to_mono(y)
|
|
zeros = np.nonzero(core.zero_crossings(y_mono))[-1]
|
|
# Force end-of-signal onto zeros
|
|
zeros = np.append(zeros, [len(y_mono)])
|
|
|
|
for interval in intervals:
|
|
if align_zeros:
|
|
interval = zeros[util.match_events(interval, zeros)]
|
|
|
|
y_out.append(y[..., interval[0] : interval[1]])
|
|
|
|
return np.concatenate(y_out, axis=-1)
|
|
|
|
|
|
def _signal_to_frame_nonsilent(
|
|
y: np.ndarray,
|
|
frame_length: int = 2048,
|
|
hop_length: int = 512,
|
|
top_db: float = 60,
|
|
ref: Union[Callable, float] = np.max,
|
|
aggregate: Callable = np.max,
|
|
) -> np.ndarray:
|
|
"""Frame-wise non-silent indicator for audio input.
|
|
|
|
This is a helper function for `trim` and `split`.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray
|
|
Audio signal, mono or stereo
|
|
|
|
frame_length : int > 0
|
|
The number of samples per frame
|
|
|
|
hop_length : int > 0
|
|
The number of samples between frames
|
|
|
|
top_db : number > 0
|
|
The threshold (in decibels) below reference to consider as
|
|
silence
|
|
|
|
ref : callable or float
|
|
The reference amplitude
|
|
|
|
aggregate : callable [default: np.max]
|
|
Function to aggregate dB measurements across channels (if y.ndim > 1)
|
|
|
|
Note: for multiple leading axes, this is performed using ``np.apply_over_axes``.
|
|
|
|
Returns
|
|
-------
|
|
non_silent : np.ndarray, shape=(m,), dtype=bool
|
|
Indicator of non-silent frames
|
|
"""
|
|
# Compute the MSE for the signal
|
|
mse = feature.rms(y=y, frame_length=frame_length, hop_length=hop_length)
|
|
|
|
# Convert to decibels and slice out the mse channel
|
|
db = core.amplitude_to_db(mse[..., 0, :], ref=ref, top_db=None)
|
|
|
|
# Aggregate everything but the time dimension
|
|
if db.ndim > 1:
|
|
db = np.apply_over_axes(aggregate, db, range(db.ndim - 1))
|
|
# Squeeze out leading singleton dimensions here
|
|
# We always want to keep the trailing dimension though
|
|
db = np.squeeze(db, axis=tuple(range(db.ndim - 1)))
|
|
|
|
return db > -top_db
|
|
|
|
|
|
def trim(
|
|
y: np.ndarray,
|
|
*,
|
|
top_db: float = 60,
|
|
ref: Union[float, Callable] = np.max,
|
|
frame_length: int = 2048,
|
|
hop_length: int = 512,
|
|
aggregate: Callable = np.max,
|
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
"""Trim leading and trailing silence from an audio signal.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray, shape=(..., n)
|
|
Audio signal. Multi-channel is supported.
|
|
top_db : number > 0
|
|
The threshold (in decibels) below reference to consider as
|
|
silence
|
|
ref : number or callable
|
|
The reference amplitude. By default, it uses `np.max` and compares
|
|
to the peak amplitude in the signal.
|
|
frame_length : int > 0
|
|
The number of samples per analysis frame
|
|
hop_length : int > 0
|
|
The number of samples between analysis frames
|
|
aggregate : callable [default: np.max]
|
|
Function to aggregate across channels (if y.ndim > 1)
|
|
|
|
Returns
|
|
-------
|
|
y_trimmed : np.ndarray, shape=(..., m)
|
|
The trimmed signal
|
|
index : np.ndarray, shape=(2,)
|
|
the interval of ``y`` corresponding to the non-silent region:
|
|
``y_trimmed = y[index[0]:index[1]]`` (for mono) or
|
|
``y_trimmed = y[:, index[0]:index[1]]`` (for stereo).
|
|
|
|
Examples
|
|
--------
|
|
>>> # Load some audio
|
|
>>> y, sr = librosa.load(librosa.ex('choice'))
|
|
>>> # Trim the beginning and ending silence
|
|
>>> yt, index = librosa.effects.trim(y)
|
|
>>> # Print the durations
|
|
>>> print(librosa.get_duration(y), librosa.get_duration(yt))
|
|
25.025986394557822 25.007891156462584
|
|
"""
|
|
non_silent = _signal_to_frame_nonsilent(
|
|
y,
|
|
frame_length=frame_length,
|
|
hop_length=hop_length,
|
|
ref=ref,
|
|
top_db=top_db,
|
|
aggregate=aggregate,
|
|
)
|
|
|
|
nonzero = np.flatnonzero(non_silent)
|
|
|
|
if nonzero.size > 0:
|
|
# Compute the start and end positions
|
|
# End position goes one frame past the last non-zero
|
|
start = int(core.frames_to_samples(nonzero[0], hop_length=hop_length))
|
|
end = min(
|
|
y.shape[-1],
|
|
int(core.frames_to_samples(nonzero[-1] + 1, hop_length=hop_length)),
|
|
)
|
|
else:
|
|
# The signal only contains zeros
|
|
start, end = 0, 0
|
|
|
|
# Build the mono/stereo index
|
|
full_index = [slice(None)] * y.ndim
|
|
full_index[-1] = slice(start, end)
|
|
|
|
return y[tuple(full_index)], np.asarray([start, end])
|
|
|
|
|
|
def split(
|
|
y: np.ndarray,
|
|
*,
|
|
top_db: float = 60,
|
|
ref: Union[float, Callable] = np.max,
|
|
frame_length: int = 2048,
|
|
hop_length: int = 512,
|
|
aggregate: Callable = np.max,
|
|
) -> np.ndarray:
|
|
"""Split an audio signal into non-silent intervals.
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray, shape=(..., n)
|
|
An audio signal. Multi-channel is supported.
|
|
top_db : number > 0
|
|
The threshold (in decibels) below reference to consider as
|
|
silence
|
|
ref : number or callable
|
|
The reference amplitude. By default, it uses `np.max` and compares
|
|
to the peak amplitude in the signal.
|
|
frame_length : int > 0
|
|
The number of samples per analysis frame
|
|
hop_length : int > 0
|
|
The number of samples between analysis frames
|
|
aggregate : callable [default: np.max]
|
|
Function to aggregate across channels (if y.ndim > 1)
|
|
|
|
Returns
|
|
-------
|
|
intervals : np.ndarray, shape=(m, 2)
|
|
``intervals[i] == (start_i, end_i)`` are the start and end time
|
|
(in samples) of non-silent interval ``i``.
|
|
"""
|
|
non_silent = _signal_to_frame_nonsilent(
|
|
y,
|
|
frame_length=frame_length,
|
|
hop_length=hop_length,
|
|
ref=ref,
|
|
top_db=top_db,
|
|
aggregate=aggregate,
|
|
)
|
|
|
|
# Interval slicing, adapted from
|
|
# https://stackoverflow.com/questions/2619413/efficiently-finding-the-interval-with-non-zeros-in-scipy-numpy-in-python
|
|
# Find points where the sign flips
|
|
edges = np.flatnonzero(np.diff(non_silent.astype(int)))
|
|
|
|
# Pad back the sample lost in the diff
|
|
edges = [edges + 1]
|
|
|
|
# If the first frame had high energy, count it
|
|
if non_silent[0]:
|
|
edges.insert(0, np.array([0]))
|
|
|
|
# Likewise for the last frame
|
|
if non_silent[-1]:
|
|
edges.append(np.array([len(non_silent)]))
|
|
|
|
# Convert from frames to samples
|
|
edges = core.frames_to_samples(np.concatenate(edges), hop_length=hop_length)
|
|
|
|
# Clip to the signal duration
|
|
edges = np.minimum(edges, y.shape[-1])
|
|
|
|
# Stack the results back as an ndarray
|
|
edges = edges.reshape((-1, 2)) # type: np.ndarray
|
|
return edges
|
|
|
|
|
|
@overload
|
|
def preemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = ...,
|
|
zi: Optional[ArrayLike] = ...,
|
|
return_zf: Literal[False] = ...,
|
|
) -> np.ndarray:
|
|
...
|
|
|
|
|
|
@overload
|
|
def preemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = ...,
|
|
zi: Optional[ArrayLike] = ...,
|
|
return_zf: Literal[True],
|
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
...
|
|
|
|
|
|
@overload
|
|
def preemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = ...,
|
|
zi: Optional[ArrayLike] = ...,
|
|
return_zf: bool,
|
|
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
|
|
...
|
|
|
|
|
|
def preemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = 0.97,
|
|
zi: Optional[ArrayLike] = None,
|
|
return_zf: bool = False,
|
|
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
|
|
"""Pre-emphasize an audio signal with a first-order differencing filter:
|
|
|
|
y[n] -> y[n] - coef * y[n-1]
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
Audio signal. Multi-channel is supported.
|
|
|
|
coef : positive number
|
|
Pre-emphasis coefficient. Typical values of ``coef`` are between 0 and 1.
|
|
|
|
At the limit ``coef=0``, the signal is unchanged.
|
|
|
|
At ``coef=1``, the result is the first-order difference of the signal.
|
|
|
|
The default (0.97) matches the pre-emphasis filter used in the HTK
|
|
implementation of MFCCs [#]_.
|
|
|
|
.. [#] https://htk.eng.cam.ac.uk/
|
|
|
|
zi : number
|
|
Initial filter state. When making successive calls to non-overlapping
|
|
frames, this can be set to the ``zf`` returned from the previous call.
|
|
(See example below.)
|
|
|
|
By default ``zi`` is initialized as ``2*y[0] - y[1]``.
|
|
|
|
return_zf : boolean
|
|
If ``True``, return the final filter state.
|
|
If ``False``, only return the pre-emphasized signal.
|
|
|
|
Returns
|
|
-------
|
|
y_out : np.ndarray
|
|
pre-emphasized signal
|
|
zf : number
|
|
if ``return_zf=True``, the final filter state is also returned
|
|
|
|
Examples
|
|
--------
|
|
Apply a standard pre-emphasis filter
|
|
|
|
>>> import matplotlib.pyplot as plt
|
|
>>> y, sr = librosa.load(librosa.ex('trumpet'))
|
|
>>> y_filt = librosa.effects.preemphasis(y)
|
|
>>> # and plot the results for comparison
|
|
>>> S_orig = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max, top_db=None)
|
|
>>> S_preemph = librosa.amplitude_to_db(np.abs(librosa.stft(y_filt)), ref=np.max, top_db=None)
|
|
>>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True)
|
|
>>> librosa.display.specshow(S_orig, y_axis='log', x_axis='time', ax=ax[0])
|
|
>>> ax[0].set(title='Original signal')
|
|
>>> ax[0].label_outer()
|
|
>>> img = librosa.display.specshow(S_preemph, y_axis='log', x_axis='time', ax=ax[1])
|
|
>>> ax[1].set(title='Pre-emphasized signal')
|
|
>>> fig.colorbar(img, ax=ax, format="%+2.f dB")
|
|
|
|
Apply pre-emphasis in pieces for block streaming. Note that the second block
|
|
initializes ``zi`` with the final state ``zf`` returned by the first call.
|
|
|
|
>>> y_filt_1, zf = librosa.effects.preemphasis(y[:1000], return_zf=True)
|
|
>>> y_filt_2, zf = librosa.effects.preemphasis(y[1000:], zi=zf, return_zf=True)
|
|
>>> np.allclose(y_filt, np.concatenate([y_filt_1, y_filt_2]))
|
|
True
|
|
|
|
See Also
|
|
--------
|
|
deemphasis
|
|
"""
|
|
b = np.asarray([1.0, -coef], dtype=y.dtype)
|
|
a = np.asarray([1.0], dtype=y.dtype)
|
|
|
|
if zi is None:
|
|
# Initialize the filter to implement linear extrapolation
|
|
zi = 2 * y[..., 0:1] - y[..., 1:2]
|
|
|
|
zi = np.atleast_1d(zi)
|
|
|
|
y_out: np.ndarray
|
|
z_f: np.ndarray
|
|
|
|
y_out, z_f = scipy.signal.lfilter(b, a, y, zi=np.asarray(zi, dtype=y.dtype))
|
|
|
|
if return_zf:
|
|
return y_out, z_f
|
|
|
|
return y_out
|
|
|
|
|
|
@overload
|
|
def deemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = ...,
|
|
zi: Optional[ArrayLike] = ...,
|
|
return_zf: Literal[False] = ...,
|
|
) -> np.ndarray:
|
|
...
|
|
|
|
|
|
@overload
|
|
def deemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = ...,
|
|
zi: Optional[ArrayLike] = ...,
|
|
return_zf: Literal[True],
|
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
...
|
|
|
|
|
|
def deemphasis(
|
|
y: np.ndarray,
|
|
*,
|
|
coef: float = 0.97,
|
|
zi: Optional[ArrayLike] = None,
|
|
return_zf: bool = False,
|
|
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
|
|
"""De-emphasize an audio signal with the inverse operation of preemphasis():
|
|
|
|
If y = preemphasis(x, coef=coef, zi=zi), the deemphasis is:
|
|
|
|
>>> x[i] = y[i] + coef * x[i-1]
|
|
>>> x = deemphasis(y, coef=coef, zi=zi)
|
|
|
|
Parameters
|
|
----------
|
|
y : np.ndarray [shape=(..., n)]
|
|
Audio signal. Multi-channel is supported.
|
|
|
|
coef : positive number
|
|
Pre-emphasis coefficient. Typical values of ``coef`` are between 0 and 1.
|
|
|
|
At the limit ``coef=0``, the signal is unchanged.
|
|
|
|
At ``coef=1``, the result is the first-order difference of the signal.
|
|
|
|
The default (0.97) matches the pre-emphasis filter used in the HTK
|
|
implementation of MFCCs [#]_.
|
|
|
|
.. [#] https://htk.eng.cam.ac.uk/
|
|
|
|
zi : number
|
|
Initial filter state. If inverting a previous preemphasis(), the same value should be used.
|
|
|
|
By default ``zi`` is initialized as
|
|
``((2 - coef) * y[0] - y[1]) / (3 - coef)``. This
|
|
value corresponds to the transformation of the default initialization of ``zi`` in ``preemphasis()``,
|
|
``2*x[0] - x[1]``.
|
|
|
|
return_zf : boolean
|
|
If ``True``, return the final filter state.
|
|
If ``False``, only return the pre-emphasized signal.
|
|
|
|
Returns
|
|
-------
|
|
y_out : np.ndarray
|
|
de-emphasized signal
|
|
zf : number
|
|
if ``return_zf=True``, the final filter state is also returned
|
|
|
|
Examples
|
|
--------
|
|
Apply a standard pre-emphasis filter and invert it with de-emphasis
|
|
|
|
>>> y, sr = librosa.load(librosa.ex('trumpet'))
|
|
>>> y_filt = librosa.effects.preemphasis(y)
|
|
>>> y_deemph = librosa.effects.deemphasis(y_filt)
|
|
>>> np.allclose(y, y_deemph)
|
|
True
|
|
|
|
See Also
|
|
--------
|
|
preemphasis
|
|
"""
|
|
b = np.array([1.0, -coef], dtype=y.dtype)
|
|
a = np.array([1.0], dtype=y.dtype)
|
|
|
|
y_out: np.ndarray
|
|
zf: np.ndarray
|
|
if zi is None:
|
|
# initialize with all zeros
|
|
zi = np.zeros(list(y.shape[:-1]) + [1], dtype=y.dtype)
|
|
y_out, zf = scipy.signal.lfilter(a, b, y, zi=zi)
|
|
|
|
# factor in the linear extrapolation
|
|
y_out -= (
|
|
((2 - coef) * y[..., 0:1] - y[..., 1:2])
|
|
/ (3 - coef)
|
|
* (coef ** np.arange(y.shape[-1]))
|
|
)
|
|
|
|
else:
|
|
zi = np.atleast_1d(zi)
|
|
y_out, zf = scipy.signal.lfilter(a, b, y, zi=zi.astype(y.dtype))
|
|
|
|
if return_zf:
|
|
return y_out, zf
|
|
else:
|
|
return y_out
|