Source code for minispec.util.utils

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Utility functions"""

import numpy as np
from numpy.lib.stride_tricks import as_strided

from .exceptions import ParameterError

# Constrain STFT block sizes to 256 KB
MAX_MEM_BLOCK = 2**8 * 2**10

__all__ = ['MAX_MEM_BLOCK',
           'frame', 'pad_center', 'fix_length',
           'valid_audio', 'normalize',
           'tiny']


[docs]def frame(y, frame_length=2048, hop_length=512): '''Slice a time series into overlapping frames. This implementation uses low-level stride manipulation to avoid redundant copies of the time series data. Parameters ---------- y : np.ndarray [shape=(n,)] Time series to frame. Must be one-dimensional and contiguous in memory. frame_length : int > 0 [scalar] Length of the frame in samples hop_length : int > 0 [scalar] Number of samples to hop between frames Returns ------- y_frames : np.ndarray [shape=(frame_length, N_FRAMES)] An array of frames sampled from `y`: `y_frames[i, j] == y[j * hop_length + i]` Raises ------ ParameterError If `y` is not contiguous in memory, not an `np.ndarray`, or not one-dimensional. See `np.ascontiguous()` for details. If `hop_length < 1`, frames cannot advance. If `len(y) < frame_length`. Examples -------- Extract 2048-sample frames from `y` with a hop of 64 samples per frame >>> y, sr = minispec.load(minispec.util.example_audio_file()) >>> minispec.util.frame(y, frame_length=2048, hop_length=64) array([[ -9.216e-06, 7.710e-06, ..., -2.117e-06, -4.362e-07], [ 2.518e-06, -6.294e-06, ..., -1.775e-05, -6.365e-06], ..., [ -7.429e-04, 5.173e-03, ..., 1.105e-05, -5.074e-06], [ 2.169e-03, 4.867e-03, ..., 3.666e-06, -5.571e-06]], dtype=float32) ''' if not isinstance(y, np.ndarray): raise ParameterError('Input must be of type numpy.ndarray, ' 'given type(y)={}'.format(type(y))) if y.ndim != 1: raise ParameterError('Input must be one-dimensional, ' 'given y.ndim={}'.format(y.ndim)) if len(y) < frame_length: raise ParameterError('Buffer is too short (n={:d})' ' for frame_length={:d}'.format(len(y), frame_length)) if hop_length < 1: raise ParameterError('Invalid hop_length: {:d}'.format(hop_length)) if not y.flags['C_CONTIGUOUS']: raise ParameterError('Input buffer must be contiguous.') # Compute the number of frames that will fit. The end may get truncated. n_frames = 1 + int((len(y) - frame_length) / hop_length) # Vertical stride is one sample # Horizontal stride is `hop_length` samples y_frames = as_strided(y, shape=(frame_length, n_frames), strides=(y.itemsize, hop_length * y.itemsize)) return y_frames
[docs]def valid_audio(y, mono=True): '''Validate whether a variable contains valid, mono audio data. Parameters ---------- y : np.ndarray The input data to validate mono : bool Whether or not to force monophonic audio Returns ------- valid : bool True if all tests pass Raises ------ ParameterError If `y` fails to meet the following criteria: - `type(y)` is `np.ndarray` - `y.dtype` is floating-point - `mono == True` and `y.ndim` is not 1 - `mono == False` and `y.ndim` is not 1 or 2 - `np.isfinite(y).all()` is not True Examples -------- >>> # Only allow monophonic signals >>> y, sr = minispec.load(minispec.util.example_audio_file()) >>> minispec.util.valid_audio(y) True >>> # If we want to allow stereo signals >>> y, sr = minispec.load(minispec.util.example_audio_file(), mono=False) >>> minispec.util.valid_audio(y, mono=False) True ''' if not isinstance(y, np.ndarray): raise ParameterError('data must be of type numpy.ndarray') if not np.issubdtype(y.dtype, np.floating): raise ParameterError('data must be floating-point') if mono and y.ndim != 1: raise ParameterError('Invalid shape for monophonic audio: ' 'ndim={:d}, shape={}'.format(y.ndim, y.shape)) elif y.ndim > 2 or y.ndim == 0: raise ParameterError('Audio must have shape (samples,) or (channels, samples). ' 'Received shape={}'.format(y.shape)) if not np.isfinite(y).all(): raise ParameterError('Audio buffer is not finite everywhere') return True
[docs]def pad_center(data, size, axis=-1, **kwargs): '''Wrapper for np.pad to automatically center an array prior to padding. This is analogous to `str.center()` Examples -------- >>> # Generate a vector >>> data = np.ones(5) >>> minispec.util.pad_center(data, 10, mode='constant') array([ 0., 0., 1., 1., 1., 1., 1., 0., 0., 0.]) >>> # Pad a matrix along its first dimension >>> data = np.ones((3, 5)) >>> minispec.util.pad_center(data, 7, axis=0) array([[ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.], [ 1., 1., 1., 1., 1.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.]]) >>> # Or its second dimension >>> minispec.util.pad_center(data, 7, axis=1) array([[ 0., 1., 1., 1., 1., 1., 0.], [ 0., 1., 1., 1., 1., 1., 0.], [ 0., 1., 1., 1., 1., 1., 0.]]) Parameters ---------- data : np.ndarray Vector to be padded and centered size : int >= len(data) [scalar] Length to pad `data` axis : int Axis along which to pad and center the data kwargs : additional keyword arguments arguments passed to `np.pad()` Returns ------- data_padded : np.ndarray `data` centered and padded to length `size` along the specified axis Raises ------ ParameterError If `size < data.shape[axis]` See Also -------- numpy.pad ''' kwargs.setdefault('mode', 'constant') n = data.shape[axis] lpad = int((size - n) // 2) lengths = [(0, 0)] * data.ndim lengths[axis] = (lpad, int(size - n - lpad)) if lpad < 0: raise ParameterError(('Target size ({:d}) must be ' 'at least input size ({:d})').format(size, n)) return np.pad(data, lengths, **kwargs)
[docs]def fix_length(data, size, axis=-1, **kwargs): '''Fix the length an array `data` to exactly `size`. If `data.shape[axis] < n`, pad according to the provided kwargs. By default, `data` is padded with trailing zeros. Examples -------- >>> y = np.arange(7) >>> # Default: pad with zeros >>> minispec.util.fix_length(y, 10) array([0, 1, 2, 3, 4, 5, 6, 0, 0, 0]) >>> # Trim to a desired length >>> minispec.util.fix_length(y, 5) array([0, 1, 2, 3, 4]) >>> # Use edge-padding instead of zeros >>> minispec.util.fix_length(y, 10, mode='edge') array([0, 1, 2, 3, 4, 5, 6, 6, 6, 6]) Parameters ---------- data : np.ndarray array to be length-adjusted size : int >= 0 [scalar] desired length of the array axis : int, <= data.ndim axis along which to fix length kwargs : additional keyword arguments Parameters to `np.pad()` Returns ------- data_fixed : np.ndarray [shape=data.shape] `data` either trimmed or padded to length `size` along the specified axis. See Also -------- numpy.pad ''' kwargs.setdefault('mode', 'constant') n = data.shape[axis] if n > size: slices = [slice(None)] * data.ndim slices[axis] = slice(0, size) return data[tuple(slices)] elif n < size: lengths = [(0, 0)] * data.ndim lengths[axis] = (0, size - n) return np.pad(data, lengths, **kwargs) return data
[docs]def normalize(S, norm=np.inf, axis=0, threshold=None, fill=None): '''Normalize an array along a chosen axis. Given a norm (described below) and a target axis, the input array is scaled so that `norm(S, axis=axis) == 1` For example, `axis=0` normalizes each column of a 2-d array by aggregating over the rows (0-axis). Similarly, `axis=1` normalizes each row of a 2-d array. This function also supports thresholding small-norm slices: any slice (i.e., row or column) with norm below a specified `threshold` can be left un-normalized, set to all-zeros, or filled with uniform non-zero values that normalize to 1. Note: the semantics of this function differ from `scipy.linalg.norm` in two ways: multi-dimensional arrays are supported, but matrix-norms are not. Parameters ---------- S : np.ndarray The matrix to normalize norm : {np.inf, -np.inf, 0, float > 0, None} - `np.inf` : maximum absolute value - `-np.inf` : mininum absolute value - `0` : number of non-zeros (the support) - float : corresponding l_p norm See `scipy.linalg.norm` for details. - None : no normalization is performed axis : int [scalar] Axis along which to compute the norm. threshold : number > 0 [optional] Only the columns (or rows) with norm at least `threshold` are normalized. By default, the threshold is determined from the numerical precision of `S.dtype`. fill : None or bool If None, then columns (or rows) with norm below `threshold` are left as is. If False, then columns (rows) with norm below `threshold` are set to 0. If True, then columns (rows) with norm below `threshold` are filled uniformly such that the corresponding norm is 1. .. note:: `fill=True` is incompatible with `norm=0` because no uniform vector exists with l0 "norm" equal to 1. Returns ------- S_norm : np.ndarray [shape=S.shape] Normalized array Raises ------ ParameterError If `norm` is not among the valid types defined above If `S` is not finite If `fill=True` and `norm=0` See Also -------- scipy.linalg.norm Examples -------- >>> # Construct an example matrix >>> S = np.vander(np.arange(-2.0, 2.0)) >>> S array([[-8., 4., -2., 1.], [-1., 1., -1., 1.], [ 0., 0., 0., 1.], [ 1., 1., 1., 1.]]) >>> # Max (l-infinity)-normalize the columns >>> minispec.util.normalize(S) array([[-1. , 1. , -1. , 1. ], [-0.125, 0.25 , -0.5 , 1. ], [ 0. , 0. , 0. , 1. ], [ 0.125, 0.25 , 0.5 , 1. ]]) >>> # Max (l-infinity)-normalize the rows >>> minispec.util.normalize(S, axis=1) array([[-1. , 0.5 , -0.25 , 0.125], [-1. , 1. , -1. , 1. ], [ 0. , 0. , 0. , 1. ], [ 1. , 1. , 1. , 1. ]]) >>> # l1-normalize the columns >>> minispec.util.normalize(S, norm=1) array([[-0.8 , 0.667, -0.5 , 0.25 ], [-0.1 , 0.167, -0.25 , 0.25 ], [ 0. , 0. , 0. , 0.25 ], [ 0.1 , 0.167, 0.25 , 0.25 ]]) >>> # l2-normalize the columns >>> minispec.util.normalize(S, norm=2) array([[-0.985, 0.943, -0.816, 0.5 ], [-0.123, 0.236, -0.408, 0.5 ], [ 0. , 0. , 0. , 0.5 ], [ 0.123, 0.236, 0.408, 0.5 ]]) >>> # Thresholding and filling >>> S[:, -1] = 1e-308 >>> S array([[ -8.000e+000, 4.000e+000, -2.000e+000, 1.000e-308], [ -1.000e+000, 1.000e+000, -1.000e+000, 1.000e-308], [ 0.000e+000, 0.000e+000, 0.000e+000, 1.000e-308], [ 1.000e+000, 1.000e+000, 1.000e+000, 1.000e-308]]) >>> # By default, small-norm columns are left untouched >>> minispec.util.normalize(S) array([[ -1.000e+000, 1.000e+000, -1.000e+000, 1.000e-308], [ -1.250e-001, 2.500e-001, -5.000e-001, 1.000e-308], [ 0.000e+000, 0.000e+000, 0.000e+000, 1.000e-308], [ 1.250e-001, 2.500e-001, 5.000e-001, 1.000e-308]]) >>> # Small-norm columns can be zeroed out >>> minispec.util.normalize(S, fill=False) array([[-1. , 1. , -1. , 0. ], [-0.125, 0.25 , -0.5 , 0. ], [ 0. , 0. , 0. , 0. ], [ 0.125, 0.25 , 0.5 , 0. ]]) >>> # Or set to constant with unit-norm >>> minispec.util.normalize(S, fill=True) array([[-1. , 1. , -1. , 1. ], [-0.125, 0.25 , -0.5 , 1. ], [ 0. , 0. , 0. , 1. ], [ 0.125, 0.25 , 0.5 , 1. ]]) >>> # With an l1 norm instead of max-norm >>> minispec.util.normalize(S, norm=1, fill=True) array([[-0.8 , 0.667, -0.5 , 0.25 ], [-0.1 , 0.167, -0.25 , 0.25 ], [ 0. , 0. , 0. , 0.25 ], [ 0.1 , 0.167, 0.25 , 0.25 ]]) ''' # Avoid div-by-zero if threshold is None: threshold = tiny(S) elif threshold <= 0: raise ParameterError('threshold={} must be strictly ' 'positive'.format(threshold)) if fill not in [None, False, True]: raise ParameterError('fill={} must be None or boolean'.format(fill)) if not np.all(np.isfinite(S)): raise ParameterError('Input must be finite') # All norms only depend on magnitude, let's do that first mag = np.abs(S).astype(np.float) # For max/min norms, filling with 1 works fill_norm = 1 if norm == np.inf: length = np.max(mag, axis=axis, keepdims=True) elif norm == -np.inf: length = np.min(mag, axis=axis, keepdims=True) elif norm == 0: if fill is True: raise ParameterError('Cannot normalize with norm=0 and fill=True') length = np.sum(mag > 0, axis=axis, keepdims=True, dtype=mag.dtype) elif np.issubdtype(type(norm), np.number) and norm > 0: length = np.sum(mag**norm, axis=axis, keepdims=True)**(1./norm) if axis is None: fill_norm = mag.size**(-1./norm) else: fill_norm = mag.shape[axis]**(-1./norm) elif norm is None: return S else: raise ParameterError('Unsupported norm: {}'.format(repr(norm))) # indices where norm is below the threshold small_idx = length < threshold Snorm = np.empty_like(S) if fill is None: # Leave small indices un-normalized length[small_idx] = 1.0 Snorm[:] = S / length elif fill: # If we have a non-zero fill value, we locate those entries by # doing a nan-divide. # If S was finite, then length is finite (except for small positions) length[small_idx] = np.nan Snorm[:] = S / length Snorm[np.isnan(Snorm)] = fill_norm else: # Set small values to zero by doing an inf-divide. # This is safe (by IEEE-754) as long as S is finite. length[small_idx] = np.inf Snorm[:] = S / length return Snorm
[docs]def tiny(x): '''Compute the tiny-value corresponding to an input's data type. This is the smallest "usable" number representable in `x`'s data type (e.g., float32). This is primarily useful for determining a threshold for numerical underflow in division or multiplication operations. Parameters ---------- x : number or np.ndarray The array to compute the tiny-value for. All that matters here is `x.dtype`. Returns ------- tiny_value : float The smallest positive usable number for the type of `x`. If `x` is integer-typed, then the tiny value for `np.float32` is returned instead. See Also -------- numpy.finfo Examples -------- For a standard double-precision floating point number: >>> minispec.util.tiny(1.0) 2.2250738585072014e-308 Or explicitly as double-precision >>> minispec.util.tiny(np.asarray(1e-5, dtype=np.float64)) 2.2250738585072014e-308 Or complex numbers >>> minispec.util.tiny(1j) 2.2250738585072014e-308 Single-precision floating point: >>> minispec.util.tiny(np.asarray(1e-5, dtype=np.float32)) 1.1754944e-38 Integer >>> minispec.util.tiny(5) 1.1754944e-38 ''' # Make sure we have an array view x = np.asarray(x) # Only floating types generate a tiny if np.issubdtype(x.dtype, np.floating) or np.issubdtype(x.dtype, np.complexfloating): dtype = x.dtype else: dtype = np.float32 return np.finfo(dtype).tiny