undetectable/voice-clone
1
1#!/usr/bin/env python2# -*- coding: utf-8 -*-3"""Utility functions"""4 5from __future__ import annotations6 7import scipy.ndimage8import scipy.sparse9 10import numpy as np11import numba12from numpy.lib.stride_tricks import as_strided13 14from .._cache import cache15from .exceptions import ParameterError16from .deprecation import Deprecated17from numpy.typing import ArrayLike, DTypeLike18from typing import (19 Any,20 Callable,21 Iterable,22 List,23 Dict,24 Optional,25 Sequence,26 Tuple,27 TypeVar,28 Union,29 overload,30)31from typing_extensions import Literal32from .._typing import _SequenceLike, _FloatLike_co, _ComplexLike_co33 34# Constrain STFT block sizes to 256 KB35MAX_MEM_BLOCK = 2**8 * 2**1036 37__all__ = [38 "MAX_MEM_BLOCK",39 "frame",40 "pad_center",41 "expand_to",42 "fix_length",43 "valid_audio",44 "valid_int",45 "is_positive_int",46 "valid_intervals",47 "fix_frames",48 "axis_sort",49 "localmax",50 "localmin",51 "normalize",52 "peak_pick",53 "sparsify_rows",54 "shear",55 "stack",56 "fill_off_diagonal",57 "index_to_slice",58 "sync",59 "softmask",60 "buf_to_float",61 "tiny",62 "cyclic_gradient",63 "dtype_r2c",64 "dtype_c2r",65 "count_unique",66 "is_unique",67 "abs2",68 "phasor",69]70 71 72def frame(73 x: np.ndarray,74 *,75 frame_length: int,76 hop_length: int,77 axis: int = -1,78 writeable: bool = False,79 subok: bool = False,80) -> np.ndarray:81 """Slice a data array into (overlapping) frames.82 83 This implementation uses low-level stride manipulation to avoid84 making a copy of the data. The resulting frame representation85 is a new view of the same input data.86 87 For example, a one-dimensional input ``x = [0, 1, 2, 3, 4, 5, 6]``88 can be framed with frame length 3 and hop length 2 in two ways.89 The first (``axis=-1``), results in the array ``x_frames``::90 91 [[0, 2, 4],92 [1, 3, 5],93 [2, 4, 6]]94 95 where each column ``x_frames[:, i]`` contains a contiguous slice of96 the input ``x[i * hop_length : i * hop_length + frame_length]``.97 98 The second way (``axis=0``) results in the array ``x_frames``::99 100 [[0, 1, 2],101 [2, 3, 4],102 [4, 5, 6]]103 104 where each row ``x_frames[i]`` contains a contiguous slice of the input.105 106 This generalizes to higher dimensional inputs, as shown in the examples below.107 In general, the framing operation increments by 1 the number of dimensions,108 adding a new "frame axis" either before the framing axis (if ``axis < 0``)109 or after the framing axis (if ``axis >= 0``).110 111 Parameters112 ----------113 x : np.ndarray114 Array to frame115 frame_length : int > 0 [scalar]116 Length of the frame117 hop_length : int > 0 [scalar]118 Number of steps to advance between frames119 axis : int120 The axis along which to frame.121 writeable : bool122 If ``True``, then the framed view of ``x`` is read-only.123 If ``False``, then the framed view is read-write. Note that writing to the framed view124 will also write to the input array ``x`` in this case.125 subok : bool126 If True, sub-classes will be passed-through, otherwise the returned array will be127 forced to be a base-class array (default).128 129 Returns130 -------131 x_frames : np.ndarray [shape=(..., frame_length, N_FRAMES, ...)]132 A framed view of ``x``, for example with ``axis=-1`` (framing on the last dimension)::133 134 x_frames[..., j] == x[..., j * hop_length : j * hop_length + frame_length]135 136 If ``axis=0`` (framing on the first dimension), then::137 138 x_frames[j] = x[j * hop_length : j * hop_length + frame_length]139 140 Raises141 ------142 ParameterError143 If ``x.shape[axis] < frame_length``, there is not enough data to fill one frame.144 145 If ``hop_length < 1``, frames cannot advance.146 147 See Also148 --------149 numpy.lib.stride_tricks.as_strided150 151 Examples152 --------153 Extract 2048-sample frames from monophonic signal with a hop of 64 samples per frame154 155 >>> y, sr = librosa.load(librosa.ex('trumpet'))156 >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64)157 >>> frames158 array([[-1.407e-03, -2.604e-02, ..., -1.795e-05, -8.108e-06],159 [-4.461e-04, -3.721e-02, ..., -1.573e-05, -1.652e-05],160 ...,161 [ 7.960e-02, -2.335e-01, ..., -6.815e-06, 1.266e-05],162 [ 9.568e-02, -1.252e-01, ..., 7.397e-06, -1.921e-05]],163 dtype=float32)164 >>> y.shape165 (117601,)166 167 >>> frames.shape168 (2048, 1806)169 170 Or frame along the first axis instead of the last:171 172 >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64, axis=0)173 >>> frames.shape174 (1806, 2048)175 176 Frame a stereo signal:177 178 >>> y, sr = librosa.load(librosa.ex('trumpet', hq=True), mono=False)179 >>> y.shape180 (2, 117601)181 >>> frames = librosa.util.frame(y, frame_length=2048, hop_length=64)182 (2, 2048, 1806)183 184 Carve an STFT into fixed-length patches of 32 frames with 50% overlap185 186 >>> y, sr = librosa.load(librosa.ex('trumpet'))187 >>> S = np.abs(librosa.stft(y))188 >>> S.shape189 (1025, 230)190 >>> S_patch = librosa.util.frame(S, frame_length=32, hop_length=16)191 >>> S_patch.shape192 (1025, 32, 13)193 >>> # The first patch contains the first 32 frames of S194 >>> np.allclose(S_patch[:, :, 0], S[:, :32])195 True196 >>> # The second patch contains frames 16 to 16+32=48, and so on197 >>> np.allclose(S_patch[:, :, 1], S[:, 16:48])198 True199 """200 201 # This implementation is derived from numpy.lib.stride_tricks.sliding_window_view (1.20.0)202 # https://numpy.org/doc/stable/reference/generated/numpy.lib.stride_tricks.sliding_window_view.html203 204 x = np.array(x, copy=False, subok=subok)205 206 if x.shape[axis] < frame_length:207 raise ParameterError(208 f"Input is too short (n={x.shape[axis]:d}) for frame_length={frame_length:d}"209 )210 211 if hop_length < 1:212 raise ParameterError(f"Invalid hop_length: {hop_length:d}")213 214 # put our new within-frame axis at the end for now215 out_strides = x.strides + tuple([x.strides[axis]])216 217 # Reduce the shape on the framing axis218 x_shape_trimmed = list(x.shape)219 x_shape_trimmed[axis] -= frame_length - 1220 221 out_shape = tuple(x_shape_trimmed) + tuple([frame_length])222 xw = as_strided(223 x, strides=out_strides, shape=out_shape, subok=subok, writeable=writeable224 )225 226 if axis < 0:227 target_axis = axis - 1228 else:229 target_axis = axis + 1230 231 xw = np.moveaxis(xw, -1, target_axis)232 233 # Downsample along the target axis234 slices = [slice(None)] * xw.ndim235 slices[axis] = slice(0, None, hop_length)236 return xw[tuple(slices)]237 238 239@cache(level=20)240def valid_audio(y: np.ndarray, *, mono: Union[bool, Deprecated] = Deprecated()) -> bool:241 """Determine whether a variable contains valid audio data.242 243 The following conditions must be satisfied:244 245 - ``type(y)`` is ``np.ndarray``246 - ``y.dtype`` is floating-point247 - ``y.ndim != 0`` (must have at least one dimension)248 - ``np.isfinite(y).all()`` samples must be all finite values249 250 If ``mono`` is specified, then we additionally require251 - ``y.ndim == 1``252 253 Parameters254 ----------255 y : np.ndarray256 The input data to validate257 258 mono : bool259 Whether or not to require monophonic audio260 261 .. warning:: The ``mono`` parameter is deprecated in version 0.9 and will be262 removed in 0.10.263 264 Returns265 -------266 valid : bool267 True if all tests pass268 269 Raises270 ------271 ParameterError272 In any of the conditions specified above fails273 274 Notes275 -----276 This function caches at level 20.277 278 Examples279 --------280 >>> # By default, valid_audio allows only mono signals281 >>> filepath = librosa.ex('trumpet', hq=True)282 >>> y_mono, sr = librosa.load(filepath, mono=True)283 >>> y_stereo, _ = librosa.load(filepath, mono=False)284 >>> librosa.util.valid_audio(y_mono), librosa.util.valid_audio(y_stereo)285 True, False286 287 >>> # To allow stereo signals, set mono=False288 >>> librosa.util.valid_audio(y_stereo, mono=False)289 True290 291 See Also292 --------293 numpy.float32294 """295 296 if not isinstance(y, np.ndarray):297 raise ParameterError("Audio data must be of type numpy.ndarray")298 299 if not np.issubdtype(y.dtype, np.floating):300 raise ParameterError("Audio data must be floating-point")301 302 if y.ndim == 0:303 raise ParameterError(304 f"Audio data must be at least one-dimensional, given y.shape={y.shape}"305 )306 307 if isinstance(mono, Deprecated):308 mono = False309 310 if mono and y.ndim != 1:311 raise ParameterError(312 f"Invalid shape for monophonic audio: ndim={y.ndim:d}, shape={y.shape}"313 )314 315 if not np.isfinite(y).all():316 raise ParameterError("Audio buffer is not finite everywhere")317 318 return True319 320 321def valid_int(x: float, *, cast: Optional[Callable[[float], float]] = None) -> int:322 """Ensure that an input value is integer-typed.323 This is primarily useful for ensuring integrable-valued324 array indices.325 326 Parameters327 ----------328 x : number329 A scalar value to be cast to int330 cast : function [optional]331 A function to modify ``x`` before casting.332 Default: `np.floor`333 334 Returns335 -------336 x_int : int337 ``x_int = int(cast(x))``338 339 Raises340 ------341 ParameterError342 If ``cast`` is provided and is not callable.343 """344 345 if cast is None:346 cast = np.floor347 348 if not callable(cast):349 raise ParameterError("cast parameter must be callable")350 351 return int(cast(x))352 353 354def is_positive_int(x: float) -> bool:355 """Checks that x is a positive integer, i.e. 1 or greater.356 357 Parameters358 ----------359 x : number360 361 Returns362 -------363 positive : bool364 365 """366 367 # Check type first to catch None values.368 return isinstance(x, (int, np.integer)) and (x > 0)369 370 371def valid_intervals(intervals: np.ndarray) -> bool:372 """Ensure that an array is a valid representation of time intervals:373 374 - intervals.ndim == 2375 - intervals.shape[1] == 2376 - intervals[i, 0] <= intervals[i, 1] for all i377 378 Parameters379 ----------380 intervals : np.ndarray [shape=(n, 2)]381 set of time intervals382 383 Returns384 -------385 valid : bool386 True if ``intervals`` passes validation.387 """388 389 if intervals.ndim != 2 or intervals.shape[-1] != 2:390 raise ParameterError("intervals must have shape (n, 2)")391 392 if np.any(intervals[:, 0] > intervals[:, 1]):393 raise ParameterError(f"intervals={intervals} must have non-negative durations")394 395 return True396 397 398def pad_center(399 data: np.ndarray, *, size: int, axis: int = -1, **kwargs: Any400) -> np.ndarray:401 """Pad an array to a target length along a target axis.402 403 This differs from `np.pad` by centering the data prior to padding,404 analogous to `str.center`405 406 Examples407 --------408 >>> # Generate a vector409 >>> data = np.ones(5)410 >>> librosa.util.pad_center(data, size=10, mode='constant')411 array([ 0., 0., 1., 1., 1., 1., 1., 0., 0., 0.])412 413 >>> # Pad a matrix along its first dimension414 >>> data = np.ones((3, 5))415 >>> librosa.util.pad_center(data, size=7, axis=0)416 array([[ 0., 0., 0., 0., 0.],417 [ 0., 0., 0., 0., 0.],418 [ 1., 1., 1., 1., 1.],419 [ 1., 1., 1., 1., 1.],420 [ 1., 1., 1., 1., 1.],421 [ 0., 0., 0., 0., 0.],422 [ 0., 0., 0., 0., 0.]])423 >>> # Or its second dimension424 >>> librosa.util.pad_center(data, size=7, axis=1)425 array([[ 0., 1., 1., 1., 1., 1., 0.],426 [ 0., 1., 1., 1., 1., 1., 0.],427 [ 0., 1., 1., 1., 1., 1., 0.]])428 429 Parameters430 ----------431 data : np.ndarray432 Vector to be padded and centered433 size : int >= len(data) [scalar]434 Length to pad ``data``435 axis : int436 Axis along which to pad and center the data437 **kwargs : additional keyword arguments438 arguments passed to `np.pad`439 440 Returns441 -------442 data_padded : np.ndarray443 ``data`` centered and padded to length ``size`` along the444 specified axis445 446 Raises447 ------448 ParameterError449 If ``size < data.shape[axis]``450 451 See Also452 --------453 numpy.pad454 """455 456 kwargs.setdefault("mode", "constant")457 458 n = data.shape[axis]459 460 lpad = int((size - n) // 2)461 462 lengths = [(0, 0)] * data.ndim463 lengths[axis] = (lpad, int(size - n - lpad))464 465 if lpad < 0:466 raise ParameterError(467 f"Target size ({size:d}) must be at least input size ({n:d})"468 )469 470 return np.pad(data, lengths, **kwargs)471 472 473def expand_to(474 x: np.ndarray, *, ndim: int, axes: Union[int, slice, Sequence[int], Sequence[slice]]475) -> np.ndarray:476 """Expand the dimensions of an input array with477 478 Parameters479 ----------480 x : np.ndarray481 The input array482 ndim : int483 The number of dimensions to expand to. Must be at least ``x.ndim``484 axes : int or slice485 The target axis or axes to preserve from x.486 All other axes will have length 1.487 488 Returns489 -------490 x_exp : np.ndarray491 The expanded version of ``x``, satisfying the following:492 ``x_exp[axes] == x``493 ``x_exp.ndim == ndim``494 495 See Also496 --------497 np.expand_dims498 499 Examples500 --------501 Expand a 1d array into an (n, 1) shape502 503 >>> x = np.arange(3)504 >>> librosa.util.expand_to(x, ndim=2, axes=0)505 array([[0],506 [1],507 [2]])508 509 Expand a 1d array into a (1, n) shape510 511 >>> librosa.util.expand_to(x, ndim=2, axes=1)512 array([[0, 1, 2]])513 514 Expand a 2d array into (1, n, m, 1) shape515 516 >>> x = np.vander(np.arange(3))517 >>> librosa.util.expand_to(x, ndim=4, axes=[1,2]).shape518 (1, 3, 3, 1)519 """520 521 # Force axes into a tuple522 axes_tup: Tuple[int]523 try:524 axes_tup = tuple(axes) # type: ignore525 except TypeError:526 axes_tup = tuple([axes]) # type: ignore527 528 if len(axes_tup) != x.ndim:529 raise ParameterError(530 f"Shape mismatch between axes={axes_tup} and input x.shape={x.shape}"531 )532 533 if ndim < x.ndim:534 raise ParameterError(535 f"Cannot expand x.shape={x.shape} to fewer dimensions ndim={ndim}"536 )537 538 shape: List[int] = [1] * ndim539 for i, axi in enumerate(axes_tup):540 shape[axi] = x.shape[i]541 542 return x.reshape(shape)543 544 545def fix_length(546 data: np.ndarray, *, size: int, axis: int = -1, **kwargs: Any547) -> np.ndarray:548 """Fix the length an array ``data`` to exactly ``size`` along a target axis.549 550 If ``data.shape[axis] < n``, pad according to the provided kwargs.551 By default, ``data`` is padded with trailing zeros.552 553 Examples554 --------555 >>> y = np.arange(7)556 >>> # Default: pad with zeros557 >>> librosa.util.fix_length(y, size=10)558 array([0, 1, 2, 3, 4, 5, 6, 0, 0, 0])559 >>> # Trim to a desired length560 >>> librosa.util.fix_length(y, size=5)561 array([0, 1, 2, 3, 4])562 >>> # Use edge-padding instead of zeros563 >>> librosa.util.fix_length(y, size=10, mode='edge')564 array([0, 1, 2, 3, 4, 5, 6, 6, 6, 6])565 566 Parameters567 ----------568 data : np.ndarray569 array to be length-adjusted570 size : int >= 0 [scalar]571 desired length of the array572 axis : int, <= data.ndim573 axis along which to fix length574 **kwargs : additional keyword arguments575 Parameters to ``np.pad``576 577 Returns578 -------579 data_fixed : np.ndarray [shape=data.shape]580 ``data`` either trimmed or padded to length ``size``581 along the specified axis.582 583 See Also584 --------585 numpy.pad586 """587 588 kwargs.setdefault("mode", "constant")589 590 n = data.shape[axis]591 592 if n > size:593 slices = [slice(None)] * data.ndim594 slices[axis] = slice(0, size)595 return data[tuple(slices)]596 597 elif n < size:598 lengths = [(0, 0)] * data.ndim599 lengths[axis] = (0, size - n)600 return np.pad(data, lengths, **kwargs)601 602 return data603 604 605def fix_frames(606 frames: _SequenceLike[int],607 *,608 x_min: Optional[int] = 0,609 x_max: Optional[int] = None,610 pad: bool = True,611) -> np.ndarray:612 """Fix a list of frames to lie within [x_min, x_max]613 614 Examples615 --------616 >>> # Generate a list of frame indices617 >>> frames = np.arange(0, 1000.0, 50)618 >>> frames619 array([ 0., 50., 100., 150., 200., 250., 300., 350.,620 400., 450., 500., 550., 600., 650., 700., 750.,621 800., 850., 900., 950.])622 >>> # Clip to span at most 250623 >>> librosa.util.fix_frames(frames, x_max=250)624 array([ 0, 50, 100, 150, 200, 250])625 >>> # Or pad to span up to 2500626 >>> librosa.util.fix_frames(frames, x_max=2500)627 array([ 0, 50, 100, 150, 200, 250, 300, 350, 400,628 450, 500, 550, 600, 650, 700, 750, 800, 850,629 900, 950, 2500])630 >>> librosa.util.fix_frames(frames, x_max=2500, pad=False)631 array([ 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500,632 550, 600, 650, 700, 750, 800, 850, 900, 950])633 634 >>> # Or starting away from zero635 >>> frames = np.arange(200, 500, 33)636 >>> frames637 array([200, 233, 266, 299, 332, 365, 398, 431, 464, 497])638 >>> librosa.util.fix_frames(frames)639 array([ 0, 200, 233, 266, 299, 332, 365, 398, 431, 464, 497])640 >>> librosa.util.fix_frames(frames, x_max=500)641 array([ 0, 200, 233, 266, 299, 332, 365, 398, 431, 464, 497,642 500])643 644 Parameters645 ----------646 frames : np.ndarray [shape=(n_frames,)]647 List of non-negative frame indices648 x_min : int >= 0 or None649 Minimum allowed frame index650 x_max : int >= 0 or None651 Maximum allowed frame index652 pad : boolean653 If ``True``, then ``frames`` is expanded to span the full range654 ``[x_min, x_max]``655 656 Returns657 -------658 fixed_frames : np.ndarray [shape=(n_fixed_frames,), dtype=int]659 Fixed frame indices, flattened and sorted660 661 Raises662 ------663 ParameterError664 If ``frames`` contains negative values665 """666 667 frames = np.asarray(frames)668 669 if np.any(frames < 0):670 raise ParameterError("Negative frame index detected")671 672 # TODO: this whole function could be made more efficient673 674 if pad and (x_min is not None or x_max is not None):675 frames = np.clip(frames, x_min, x_max)676 677 if pad:678 pad_data = []679 if x_min is not None:680 pad_data.append(x_min)681 if x_max is not None:682 pad_data.append(x_max)683 frames = np.concatenate((np.asarray(pad_data), frames))684 685 if x_min is not None:686 frames = frames[frames >= x_min]687 688 if x_max is not None:689 frames = frames[frames <= x_max]690 691 unique: np.ndarray = np.unique(frames).astype(int)692 return unique693 694 695@overload696def axis_sort(697 S: np.ndarray,698 *,699 axis: int = ...,700 index: Literal[False] = ...,701 value: Optional[Callable[..., Any]] = ...,702) -> np.ndarray:703 ...704 705 706@overload707def axis_sort(708 S: np.ndarray,709 *,710 axis: int = ...,711 index: Literal[True],712 value: Optional[Callable[..., Any]] = ...,713) -> Tuple[np.ndarray, np.ndarray]:714 ...715 716 717def axis_sort(718 S: np.ndarray,719 *,720 axis: int = -1,721 index: bool = False,722 value: Optional[Callable[..., Any]] = None,723) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:724 """Sort an array along its rows or columns.725 726 Examples727 --------728 Visualize NMF output for a spectrogram S729 730 >>> # Sort the columns of W by peak frequency bin731 >>> y, sr = librosa.load(librosa.ex('trumpet'))732 >>> S = np.abs(librosa.stft(y))733 >>> W, H = librosa.decompose.decompose(S, n_components=64)734 >>> W_sort = librosa.util.axis_sort(W)735 736 Or sort by the lowest frequency bin737 738 >>> W_sort = librosa.util.axis_sort(W, value=np.argmin)739 740 Or sort the rows instead of the columns741 742 >>> W_sort_rows = librosa.util.axis_sort(W, axis=0)743 744 Get the sorting index also, and use it to permute the rows of H745 746 >>> W_sort, idx = librosa.util.axis_sort(W, index=True)747 >>> H_sort = H[idx, :]748 749 >>> import matplotlib.pyplot as plt750 >>> fig, ax = plt.subplots(nrows=2, ncols=2)751 >>> img_w = librosa.display.specshow(librosa.amplitude_to_db(W, ref=np.max),752 ... y_axis='log', ax=ax[0, 0])753 >>> ax[0, 0].set(title='W')754 >>> ax[0, 0].label_outer()755 >>> img_act = librosa.display.specshow(H, x_axis='time', ax=ax[0, 1])756 >>> ax[0, 1].set(title='H')757 >>> ax[0, 1].label_outer()758 >>> librosa.display.specshow(librosa.amplitude_to_db(W_sort,759 ... ref=np.max),760 ... y_axis='log', ax=ax[1, 0])761 >>> ax[1, 0].set(title='W sorted')762 >>> librosa.display.specshow(H_sort, x_axis='time', ax=ax[1, 1])763 >>> ax[1, 1].set(title='H sorted')764 >>> ax[1, 1].label_outer()765 >>> fig.colorbar(img_w, ax=ax[:, 0], orientation='horizontal')766 >>> fig.colorbar(img_act, ax=ax[:, 1], orientation='horizontal')767 768 Parameters769 ----------770 S : np.ndarray [shape=(d, n)]771 Array to be sorted772 773 axis : int [scalar]774 The axis along which to compute the sorting values775 776 - ``axis=0`` to sort rows by peak column index777 - ``axis=1`` to sort columns by peak row index778 779 index : boolean [scalar]780 If true, returns the index array as well as the permuted data.781 782 value : function783 function to return the index corresponding to the sort order.784 Default: `np.argmax`.785 786 Returns787 -------788 S_sort : np.ndarray [shape=(d, n)]789 ``S`` with the columns or rows permuted in sorting order790 idx : np.ndarray (optional) [shape=(d,) or (n,)]791 If ``index == True``, the sorting index used to permute ``S``.792 Length of ``idx`` corresponds to the selected ``axis``.793 794 Raises795 ------796 ParameterError797 If ``S`` does not have exactly 2 dimensions (``S.ndim != 2``)798 """799 800 if value is None:801 value = np.argmax802 803 if S.ndim != 2:804 raise ParameterError("axis_sort is only defined for 2D arrays")805 806 bin_idx = value(S, axis=np.mod(1 - axis, S.ndim))807 idx = np.argsort(bin_idx)808 809 sort_slice = [slice(None)] * S.ndim810 sort_slice[axis] = idx # type: ignore811 812 if index:813 return S[tuple(sort_slice)], idx814 else:815 return S[tuple(sort_slice)]816 817 818@cache(level=40)819def normalize(820 S: np.ndarray,821 *,822 norm: Optional[float] = np.inf,823 axis: Optional[int] = 0,824 threshold: Optional[_FloatLike_co] = None,825 fill: Optional[bool] = None,826) -> np.ndarray:827 """Normalize an array along a chosen axis.828 829 Given a norm (described below) and a target axis, the input830 array is scaled so that::831 832 norm(S, axis=axis) == 1833 834 For example, ``axis=0`` normalizes each column of a 2-d array835 by aggregating over the rows (0-axis).836 Similarly, ``axis=1`` normalizes each row of a 2-d array.837 838 This function also supports thresholding small-norm slices:839 any slice (i.e., row or column) with norm below a specified840 ``threshold`` can be left un-normalized, set to all-zeros, or841 filled with uniform non-zero values that normalize to 1.842 843 Note: the semantics of this function differ from844 `scipy.linalg.norm` in two ways: multi-dimensional arrays845 are supported, but matrix-norms are not.846 847 Parameters848 ----------849 S : np.ndarray850 The array to normalize851 852 norm : {np.inf, -np.inf, 0, float > 0, None}853 - `np.inf` : maximum absolute value854 - `-np.inf` : minimum absolute value855 - `0` : number of non-zeros (the support)856 - float : corresponding l_p norm857 See `scipy.linalg.norm` for details.858 - None : no normalization is performed859 860 axis : int [scalar]861 Axis along which to compute the norm.862 863 threshold : number > 0 [optional]864 Only the columns (or rows) with norm at least ``threshold`` are865 normalized.866 867 By default, the threshold is determined from868 the numerical precision of ``S.dtype``.869 870 fill : None or bool871 If None, then columns (or rows) with norm below ``threshold``872 are left as is.873 874 If False, then columns (rows) with norm below ``threshold``875 are set to 0.876 877 If True, then columns (rows) with norm below ``threshold``878 are filled uniformly such that the corresponding norm is 1.879 880 .. note:: ``fill=True`` is incompatible with ``norm=0`` because881 no uniform vector exists with l0 "norm" equal to 1.882 883 Returns884 -------885 S_norm : np.ndarray [shape=S.shape]886 Normalized array887 888 Raises889 ------890 ParameterError891 If ``norm`` is not among the valid types defined above892 893 If ``S`` is not finite894 895 If ``fill=True`` and ``norm=0``896 897 See Also898 --------899 scipy.linalg.norm900 901 Notes902 -----903 This function caches at level 40.904 905 Examples906 --------907 >>> # Construct an example matrix908 >>> S = np.vander(np.arange(-2.0, 2.0))909 >>> S910 array([[-8., 4., -2., 1.],911 [-1., 1., -1., 1.],912 [ 0., 0., 0., 1.],913 [ 1., 1., 1., 1.]])914 >>> # Max (l-infinity)-normalize the columns915 >>> librosa.util.normalize(S)916 array([[-1. , 1. , -1. , 1. ],917 [-0.125, 0.25 , -0.5 , 1. ],918 [ 0. , 0. , 0. , 1. ],919 [ 0.125, 0.25 , 0.5 , 1. ]])920 >>> # Max (l-infinity)-normalize the rows921 >>> librosa.util.normalize(S, axis=1)922 array([[-1. , 0.5 , -0.25 , 0.125],923 [-1. , 1. , -1. , 1. ],924 [ 0. , 0. , 0. , 1. ],925 [ 1. , 1. , 1. , 1. ]])926 >>> # l1-normalize the columns927 >>> librosa.util.normalize(S, norm=1)928 array([[-0.8 , 0.667, -0.5 , 0.25 ],929 [-0.1 , 0.167, -0.25 , 0.25 ],930 [ 0. , 0. , 0. , 0.25 ],931 [ 0.1 , 0.167, 0.25 , 0.25 ]])932 >>> # l2-normalize the columns933 >>> librosa.util.normalize(S, norm=2)934 array([[-0.985, 0.943, -0.816, 0.5 ],935 [-0.123, 0.236, -0.408, 0.5 ],936 [ 0. , 0. , 0. , 0.5 ],937 [ 0.123, 0.236, 0.408, 0.5 ]])938 939 >>> # Thresholding and filling940 >>> S[:, -1] = 1e-308941 >>> S942 array([[ -8.000e+000, 4.000e+000, -2.000e+000,943 1.000e-308],944 [ -1.000e+000, 1.000e+000, -1.000e+000,945 1.000e-308],946 [ 0.000e+000, 0.000e+000, 0.000e+000,947 1.000e-308],948 [ 1.000e+000, 1.000e+000, 1.000e+000,949 1.000e-308]])950 951 >>> # By default, small-norm columns are left untouched952 >>> librosa.util.normalize(S)953 array([[ -1.000e+000, 1.000e+000, -1.000e+000,954 1.000e-308],955 [ -1.250e-001, 2.500e-001, -5.000e-001,956 1.000e-308],957 [ 0.000e+000, 0.000e+000, 0.000e+000,958 1.000e-308],959 [ 1.250e-001, 2.500e-001, 5.000e-001,960 1.000e-308]])961 >>> # Small-norm columns can be zeroed out962 >>> librosa.util.normalize(S, fill=False)963 array([[-1. , 1. , -1. , 0. ],964 [-0.125, 0.25 , -0.5 , 0. ],965 [ 0. , 0. , 0. , 0. ],966 [ 0.125, 0.25 , 0.5 , 0. ]])967 >>> # Or set to constant with unit-norm968 >>> librosa.util.normalize(S, fill=True)969 array([[-1. , 1. , -1. , 1. ],970 [-0.125, 0.25 , -0.5 , 1. ],971 [ 0. , 0. , 0. , 1. ],972 [ 0.125, 0.25 , 0.5 , 1. ]])973 >>> # With an l1 norm instead of max-norm974 >>> librosa.util.normalize(S, norm=1, fill=True)975 array([[-0.8 , 0.667, -0.5 , 0.25 ],976 [-0.1 , 0.167, -0.25 , 0.25 ],977 [ 0. , 0. , 0. , 0.25 ],978 [ 0.1 , 0.167, 0.25 , 0.25 ]])979 """980 981 # Avoid div-by-zero982 if threshold is None:983 threshold = tiny(S)984 985 elif threshold <= 0:986 raise ParameterError(f"threshold={threshold} must be strictly positive")987 988 if fill not in [None, False, True]:989 raise ParameterError(f"fill={fill} must be None or boolean")990 991 if not np.all(np.isfinite(S)):992 raise ParameterError("Input must be finite")993 994 # All norms only depend on magnitude, let's do that first995 mag = np.abs(S).astype(float)996 997 # For max/min norms, filling with 1 works998 fill_norm = 1999 1000 if norm is None:1001 return S1002 1003 elif norm == np.inf:1004 length = np.max(mag, axis=axis, keepdims=True)1005 1006 elif norm == -np.inf:1007 length = np.min(mag, axis=axis, keepdims=True)1008 1009 elif norm == 0:1010 if fill is True:1011 raise ParameterError("Cannot normalize with norm=0 and fill=True")1012 1013 length = np.sum(mag > 0, axis=axis, keepdims=True, dtype=mag.dtype)1014 1015 elif np.issubdtype(type(norm), np.number) and norm > 0:1016 length = np.sum(mag**norm, axis=axis, keepdims=True) ** (1.0 / norm)1017 1018 if axis is None:1019 fill_norm = mag.size ** (-1.0 / norm)1020 else:1021 fill_norm = mag.shape[axis] ** (-1.0 / norm)1022 1023 else:1024 raise ParameterError(f"Unsupported norm: {repr(norm)}")1025 1026 # indices where norm is below the threshold1027 small_idx = length < threshold1028 1029 Snorm = np.empty_like(S)1030 if fill is None:1031 # Leave small indices un-normalized1032 length[small_idx] = 1.01033 Snorm[:] = S / length1034 1035 elif fill:1036 # If we have a non-zero fill value, we locate those entries by1037 # doing a nan-divide.1038 # If S was finite, then length is finite (except for small positions)1039 length[small_idx] = np.nan1040 Snorm[:] = S / length1041 Snorm[np.isnan(Snorm)] = fill_norm1042 else:1043 # Set small values to zero by doing an inf-divide.1044 # This is safe (by IEEE-754) as long as S is finite.1045 length[small_idx] = np.inf1046 Snorm[:] = S / length1047 1048 return Snorm1049 1050 1051@numba.stencil1052def _localmax_sten(x): # pragma: no cover1053 """Numba stencil for local maxima computation"""1054 return (x[0] > x[-1]) & (x[0] >= x[1])1055 1056 1057@numba.stencil1058def _localmin_sten(x): # pragma: no cover1059 """Numba stencil for local minima computation"""1060 return (x[0] < x[-1]) & (x[0] <= x[1])1061 1062 1063@numba.guvectorize(1064 [1065 "void(int16[:], bool_[:])",1066 "void(int32[:], bool_[:])",1067 "void(int64[:], bool_[:])",1068 "void(float32[:], bool_[:])",1069 "void(float64[:], bool_[:])",1070 ],1071 "(n)->(n)",1072 cache=False,1073 nopython=True,1074)1075def _localmax(x, y): # pragma: no cover1076 """Vectorized wrapper for the localmax stencil"""1077 y[:] = _localmax_sten(x)1078 1079 1080@numba.guvectorize(1081 [1082 "void(int16[:], bool_[:])",1083 "void(int32[:], bool_[:])",1084 "void(int64[:], bool_[:])",1085 "void(float32[:], bool_[:])",1086 "void(float64[:], bool_[:])",1087 ],1088 "(n)->(n)",1089 cache=False,1090 nopython=True,1091)1092def _localmin(x, y): # pragma: no cover1093 """Vectorized wrapper for the localmin stencil"""1094 y[:] = _localmin_sten(x)1095 1096 1097def localmax(x: np.ndarray, *, axis: int = 0) -> np.ndarray:1098 """Find local maxima in an array1099 1100 An element ``x[i]`` is considered a local maximum if the following1101 conditions are met:1102 1103 - ``x[i] > x[i-1]``1104 - ``x[i] >= x[i+1]``1105 1106 Note that the first condition is strict, and that the first element1107 ``x[0]`` will never be considered as a local maximum.1108 1109 Examples1110 --------1111 >>> x = np.array([1, 0, 1, 2, -1, 0, -2, 1])1112 >>> librosa.util.localmax(x)1113 array([False, False, False, True, False, True, False, True], dtype=bool)1114 1115 >>> # Two-dimensional example1116 >>> x = np.array([[1,0,1], [2, -1, 0], [2, 1, 3]])1117 >>> librosa.util.localmax(x, axis=0)1118 array([[False, False, False],1119 [ True, False, False],1120 [False, True, True]], dtype=bool)1121 >>> librosa.util.localmax(x, axis=1)1122 array([[False, False, True],1123 [False, False, True],1124 [False, False, True]], dtype=bool)1125 1126 Parameters1127 ----------1128 x : np.ndarray [shape=(d1,d2,...)]1129 input vector or array1130 axis : int1131 axis along which to compute local maximality1132 1133 Returns1134 -------1135 m : np.ndarray [shape=x.shape, dtype=bool]1136 indicator array of local maximality along ``axis``1137 1138 See Also1139 --------1140 localmin1141 """1142 # Rotate the target axis to the end1143 xi = x.swapaxes(-1, axis)1144 1145 # Allocate the output array and rotate target axis1146 lmax = np.empty_like(x, dtype=bool)1147 lmaxi = lmax.swapaxes(-1, axis)1148 1149 # Call the vectorized stencil1150 _localmax(xi, lmaxi)1151 1152 # Handle the edge condition not covered by the stencil1153 lmaxi[..., -1] = xi[..., -1] > xi[..., -2]1154 1155 return lmax1156 1157 1158def localmin(x: np.ndarray, *, axis: int = 0) -> np.ndarray:1159 """Find local minima in an array1160 1161 An element ``x[i]`` is considered a local minimum if the following1162 conditions are met:1163 1164 - ``x[i] < x[i-1]``1165 - ``x[i] <= x[i+1]``1166 1167 Note that the first condition is strict, and that the first element1168 ``x[0]`` will never be considered as a local minimum.1169 1170 Examples1171 --------1172 >>> x = np.array([1, 0, 1, 2, -1, 0, -2, 1])1173 >>> librosa.util.localmin(x)1174 array([False, True, False, False, True, False, True, False])1175 1176 >>> # Two-dimensional example1177 >>> x = np.array([[1,0,1], [2, -1, 0], [2, 1, 3]])1178 >>> librosa.util.localmin(x, axis=0)1179 array([[False, False, False],1180 [False, True, True],1181 [False, False, False]])1182 1183 >>> librosa.util.localmin(x, axis=1)1184 array([[False, True, False],1185 [False, True, False],1186 [False, True, False]])1187 1188 Parameters1189 ----------1190 x : np.ndarray [shape=(d1,d2,...)]1191 input vector or array1192 axis : int1193 axis along which to compute local minimality1194 1195 Returns1196 -------1197 m : np.ndarray [shape=x.shape, dtype=bool]1198 indicator array of local minimality along ``axis``1199 1200 See Also