| |
| |
| """Harmonic calculations for frequency representations""" |
|
|
| import warnings |
|
|
| import numpy as np |
| import scipy.interpolate |
| import scipy.signal |
| from ..util.exceptions import ParameterError |
| from ..util import is_unique |
| from ..util.decorators import deprecate_positional_args |
|
|
| __all__ = ["salience", "interp_harmonics"] |
|
|
|
|
| @deprecate_positional_args |
| def salience( |
| S, |
| *, |
| freqs, |
| harmonics, |
| weights=None, |
| aggregate=None, |
| filter_peaks=True, |
| fill_value=np.nan, |
| kind="linear", |
| axis=-2, |
| ): |
| """Harmonic salience function. |
| |
| Parameters |
| ---------- |
| S : np.ndarray [shape=(..., d, n)] |
| input time frequency magnitude representation (e.g. STFT or CQT magnitudes). |
| Must be real-valued and non-negative. |
| |
| freqs : np.ndarray, shape=(S.shape[axis]) |
| The frequency values corresponding to S's elements along the |
| chosen axis. |
| |
| harmonics : list-like, non-negative |
| Harmonics to include in salience computation. The first harmonic (1) |
| corresponds to ``S`` itself. Values less than one (e.g., 1/2) correspond |
| to sub-harmonics. |
| |
| weights : list-like |
| The weight to apply to each harmonic in the summation. (default: |
| uniform weights). Must be the same length as ``harmonics``. |
| |
| aggregate : function |
| aggregation function (default: `np.average`) |
| |
| If ``aggregate=np.average``, then a weighted average is |
| computed per-harmonic according to the specified weights. |
| For all other aggregation functions, all harmonics |
| are treated equally. |
| |
| filter_peaks : bool |
| If true, returns harmonic summation only on frequencies of peak |
| magnitude. Otherwise returns harmonic summation over the full spectrum. |
| Defaults to True. |
| |
| fill_value : float |
| The value to fill non-peaks in the output representation. (default: |
| `np.nan`) Only used if ``filter_peaks == True``. |
| |
| kind : str |
| Interpolation type for harmonic estimation. |
| See `scipy.interpolate.interp1d`. |
| |
| axis : int |
| The axis along which to compute harmonics |
| |
| Returns |
| ------- |
| S_sal : np.ndarray |
| ``S_sal`` will have the same shape as ``S``, and measure |
| the overall harmonic energy at each frequency. |
| |
| See Also |
| -------- |
| interp_harmonics |
| |
| Examples |
| -------- |
| >>> y, sr = librosa.load(librosa.ex('trumpet'), duration=3) |
| >>> S = np.abs(librosa.stft(y)) |
| >>> freqs = librosa.fft_frequencies(sr=sr) |
| >>> harms = [1, 2, 3, 4] |
| >>> weights = [1.0, 0.5, 0.33, 0.25] |
| >>> S_sal = librosa.salience(S, freqs=freqs, harmonics=harms, weights=weights, fill_value=0) |
| >>> print(S_sal.shape) |
| (1025, 115) |
| >>> import matplotlib.pyplot as plt |
| >>> fig, ax = plt.subplots(nrows=2, sharex=True, sharey=True) |
| >>> librosa.display.specshow(librosa.amplitude_to_db(S, ref=np.max), |
| ... sr=sr, y_axis='log', x_axis='time', ax=ax[0]) |
| >>> ax[0].set(title='Magnitude spectrogram') |
| >>> ax[0].label_outer() |
| >>> img = librosa.display.specshow(librosa.amplitude_to_db(S_sal, |
| ... ref=np.max), |
| ... sr=sr, y_axis='log', x_axis='time', ax=ax[1]) |
| >>> ax[1].set(title='Salience spectrogram') |
| >>> fig.colorbar(img, ax=ax, format="%+2.0f dB") |
| """ |
| if aggregate is None: |
| aggregate = np.average |
|
|
| if weights is None: |
| weights = np.ones((len(harmonics),)) |
| else: |
| weights = np.array(weights, dtype=float) |
|
|
| S_harm = interp_harmonics(S, freqs=freqs, harmonics=harmonics, kind=kind, axis=axis) |
|
|
| if aggregate is np.average: |
| S_sal = aggregate(S_harm, axis=axis - 1, weights=weights) |
| else: |
| S_sal = aggregate(S_harm, axis=axis - 1) |
|
|
| if filter_peaks: |
| S_peaks = scipy.signal.argrelmax(S, axis=axis) |
| S_out = np.empty(S.shape) |
| S_out.fill(fill_value) |
| S_out[S_peaks] = S_sal[S_peaks] |
|
|
| S_sal = S_out |
|
|
| return S_sal |
|
|
|
|
| @deprecate_positional_args |
| def interp_harmonics(x, *, freqs, harmonics, kind="linear", fill_value=0, axis=-2): |
| """Compute the energy at harmonics of time-frequency representation. |
| |
| Given a frequency-based energy representation such as a spectrogram |
| or tempogram, this function computes the energy at the chosen harmonics |
| of the frequency axis. (See examples below.) |
| The resulting harmonic array can then be used as input to a salience |
| computation. |
| |
| Parameters |
| ---------- |
| x : np.ndarray |
| The input energy |
| freqs : np.ndarray, shape=(X.shape[axis]) |
| The frequency values corresponding to X's elements along the |
| chosen axis. |
| harmonics : list-like, non-negative |
| Harmonics to compute as ``harmonics[i] * freqs``. |
| The first harmonic (1) corresponds to ``freqs``. |
| Values less than one (e.g., 1/2) correspond to sub-harmonics. |
| kind : str |
| Interpolation type. See `scipy.interpolate.interp1d`. |
| fill_value : float |
| The value to fill when extrapolating beyond the observed |
| frequency range. |
| axis : int |
| The axis along which to compute harmonics |
| |
| Returns |
| ------- |
| x_harm : np.ndarray |
| ``x_harm[i]`` will have the same shape as ``x``, and measure |
| the energy at the ``harmonics[i]`` harmonic of each frequency. |
| A new dimension indexing harmonics will be inserted immediately |
| before ``axis``. |
| |
| See Also |
| -------- |
| scipy.interpolate.interp1d |
| |
| Examples |
| -------- |
| Estimate the harmonics of a time-averaged tempogram |
| |
| >>> y, sr = librosa.load(librosa.ex('sweetwaltz')) |
| >>> # Compute the time-varying tempogram and average over time |
| >>> tempi = np.mean(librosa.feature.tempogram(y=y, sr=sr), axis=1) |
| >>> # We'll measure the first five harmonics |
| >>> harmonics = [1, 2, 3, 4, 5] |
| >>> f_tempo = librosa.tempo_frequencies(len(tempi), sr=sr) |
| >>> # Build the harmonic tensor; we only have one axis here (tempo) |
| >>> t_harmonics = librosa.interp_harmonics(tempi, freqs=f_tempo, harmonics=harmonics, axis=0) |
| >>> print(t_harmonics.shape) |
| (5, 384) |
| |
| >>> # And plot the results |
| >>> import matplotlib.pyplot as plt |
| >>> fig, ax = plt.subplots() |
| >>> librosa.display.specshow(t_harmonics, x_axis='tempo', sr=sr, ax=ax) |
| >>> ax.set(yticks=np.arange(len(harmonics)), |
| ... yticklabels=['{:.3g}'.format(_) for _ in harmonics], |
| ... ylabel='Harmonic', xlabel='Tempo (BPM)') |
| |
| We can also compute frequency harmonics for spectrograms. |
| To calculate sub-harmonic energy, use values < 1. |
| |
| >>> y, sr = librosa.load(librosa.ex('trumpet'), duration=3) |
| >>> harmonics = [1./3, 1./2, 1, 2, 3, 4] |
| >>> S = np.abs(librosa.stft(y)) |
| >>> fft_freqs = librosa.fft_frequencies(sr=sr) |
| >>> S_harm = librosa.interp_harmonics(S, freqs=fft_freqs, harmonics=harmonics, axis=0) |
| >>> print(S_harm.shape) |
| (6, 1025, 646) |
| |
| >>> fig, ax = plt.subplots(nrows=3, ncols=2, sharex=True, sharey=True) |
| >>> for i, _sh in enumerate(S_harm): |
| ... img = librosa.display.specshow(librosa.amplitude_to_db(_sh, |
| ... ref=S.max()), |
| ... sr=sr, y_axis='log', x_axis='time', |
| ... ax=ax.flat[i]) |
| ... ax.flat[i].set(title='h={:.3g}'.format(harmonics[i])) |
| ... ax.flat[i].label_outer() |
| >>> fig.colorbar(img, ax=ax, format="%+2.f dB") |
| """ |
|
|
| if freqs.ndim == 1 and len(freqs) == x.shape[axis]: |
| |
| |
|
|
| |
| if not is_unique(freqs, axis=0): |
| warnings.warn( |
| "Frequencies are not unique. This may produce incorrect harmonic interpolations.", |
| stacklevel=2, |
| ) |
|
|
| f_interp = scipy.interpolate.interp1d( |
| freqs, |
| x, |
| axis=axis, |
| bounds_error=False, |
| copy=False, |
| kind=kind, |
| fill_value=fill_value, |
| ) |
|
|
| |
| f_out = np.multiply.outer(harmonics, freqs) |
|
|
| |
| return f_interp(f_out) |
|
|
| elif freqs.shape == x.shape: |
| if not np.all(is_unique(freqs, axis=axis)): |
| warnings.warn( |
| "Frequencies are not unique. This may produce incorrect harmonic interpolations.", |
| stacklevel=2, |
| ) |
|
|
| |
|
|
| |
| |
| def _f_interp(_a, _b): |
| interp = scipy.interpolate.interp1d( |
| _a, _b, bounds_error=False, copy=False, kind=kind, fill_value=fill_value |
| ) |
|
|
| return interp(np.multiply.outer(_a, harmonics)) |
|
|
| |
| xfunc = np.vectorize(_f_interp, signature="(f),(f)->(f,h)") |
|
|
| |
| |
| |
| return ( |
| xfunc(freqs.swapaxes(axis, -1), x.swapaxes(axis, -1)) |
| .swapaxes( |
| |
| -2, |
| axis, |
| ) |
| .swapaxes( |
| |
| -1, |
| axis - 1, |
| ) |
| ) |
| else: |
| raise ParameterError( |
| "freqs.shape={} does not match " |
| "input shape={}".format(freqs.shape, x.shape) |
| ) |
|
|