#!/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