# Copyright (c) 2023, Trustees of the University of Pennsylvania
# See LICENSE for licensing conditions
"""Miscellaneous utility functions related to audio and segmentation."""
import dataclasses
import os
from pathlib import Path
from typing import Iterable
import wave
import numpy as np
import scipy.signal
__all__ = ['add_dataclass_slots', 'clip', 'get_nframes_wav', 'resample',
'which']
[docs]def resample(x, orig_sr, new_sr):
"""Resample audio from `orig_sr` to `new_sr` Hz.
Uses polyphase resampling as implemented within :mod:`scipy.signal`.
Parameters
----------
x : numpy.ndarray, (n_samples,)
Time series to be resampled.
orig_sr : int
Original sample rate (Hz) of `x`.
new_sr : int
New sample rate (Hz).
Returns
-------
x_resamp : numpy.ndarray, (n_samples * new_sr / orig_sr,)
Version of `x` resampled from `orig_sr` Hz to `new_sr` Hz.
See also
--------
scipy.signal.resample_poly
"""
gcd = np.gcd(orig_sr, new_sr)
upsample_factor = new_sr // gcd
downsample_factor = orig_sr // gcd
return scipy.signal.resample_poly(
x, upsample_factor, downsample_factor, axis=-1)
[docs]def clip(x, lb, ub):
"""Clip `x` to interval [`lb`, `ub`]."""
if ub <= lb:
raise ValueError(f'Invalid clipping interval: [{lb}, {ub}].')
return max(lb, min(x, ub))
[docs]def add_dataclass_slots(cls):
"""Add `__slots__` to a data class.
Notes
-----
https://github.com/ericvsmith/dataclasses/blob/master/dataclass_tools.py
"""
# Need to create a new class, since we can't set __slots__
# after a class has been created.
# Make sure __slots__ isn't already set.
if '__slots__' in cls.__dict__:
raise TypeError(f'{cls.__name__} already specifies __slots__')
# Create a new dict for our new class.
cls_dict = dict(cls.__dict__)
field_names = tuple(f.name for f in dataclasses.fields(cls))
cls_dict['__slots__'] = field_names
for field_name in field_names:
# Remove our attributes, if present. They'll still be
# available in _MARKER.
cls_dict.pop(field_name, None)
# Remove __dict__ itself.
cls_dict.pop('__dict__', None)
# And finally create the class.
qualname = getattr(cls, '__qualname__', None)
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
if qualname is not None:
cls.__qualname__ = qualname
return cls
[docs]def which(program, search_dirs=None):
"""Returns path to excutable `program`.
If `program` is not found on the user's PATH, returns ``None``.
Parameters
----------
program : str
Name of program to search for.
search_dirs : Iterable[pathlib.Path], optional
List of additional directories to search. These directories will be
searched in order **BEFORE** the user's PATH.
(Default: None)
"""
def is_exe(fpath):
return fpath.is_file() and os.access(fpath, os.X_OK)
program = Path(program)
if search_dirs is None:
search_dirs = []
search_dirs = list(search_dirs)
search_dirs += os.environ['PATH'].split(os.pathsep)
if is_exe(program):
return program
for dirpath in search_dirs:
fpath = Path(dirpath, program)
if is_exe(fpath):
return fpath
return None
def get_nframes_wav(fpath):
"""Return number of frames in WAV file."""
with wave.open(str(fpath), 'r') as f:
return f.getnframes()