Source code for vocalpy.spectrogram_maker

"""Class that represents the step in a pipeline that makes spectrograms from audio."""

from __future__ import annotations

import collections.abc
import inspect
from typing import Callable, List, Mapping, Sequence

import dask
import dask.diagnostics

from ._spectrogram.data_type import Spectrogram
from .audio_file import AudioFile
from .params import Params
from .sound import Sound


def validate_sound(
    sound: Sound | AudioFile | Sequence[Sound | AudioFile],
) -> None:
    if not isinstance(sound, (Sound, AudioFile, list, tuple)):
        raise TypeError(
            "`sound` must be a `vocalpy.Sound` instance, "
            "a `vocalpy.AudioFile` instance, "
            "or a list/tuple of such instances, "
            f"but type was : {type(sound)}"
        )

    if isinstance(sound, list) or isinstance(sound, tuple):
        if not (
            all([isinstance(item, Sound) for item in sound])
            or all([isinstance(item, AudioFile) for item in sound])
        ):
            types_in_sound = set([type(sound) for sound in sound])
            raise TypeError(
                "If `sound` is a list or tuple, "
                "then items in `sound` must either "
                "all be instances of `vocalpy.Sound`"
                "or all be instances of `vocalpy.AudioFile`."
                f"Instead found the following types: {types_in_sound}."
                f"Please make sure only `vocalpy.Sound instances are in the list/tuple."
            )


DEFAULT_SPECT_PARAMS = {"n_fft": 512, "hop_length": 64}


[docs] class SpectrogramMaker: """Class that represents the step in a pipeline that makes spectrograms from audio. Attributes ---------- callback : Callable Callable that accepts a :class:`Sound` and returns a :class:`Spectrogram`. Default is :func:`vocalpy.spectrogram`. params : dict Parameters for making spectrograms. Passed as keyword arguments to ``callback``. """
[docs] def __init__( self, callback: Callable | None = None, params: Mapping | Params | None = None, ): if callback is None: import vocalpy.spectrogram callback = vocalpy.spectrogram # if callback was None and we use the default, # **and** params is None, we set these default params if params is None: params = DEFAULT_SPECT_PARAMS else: # if we *don't* use the default callback **and** params is None, # then we instead get the defaults for the specified callback if params is None: params = {} signature = inspect.signature(callback) for name, param in signature.parameters.items(): if param.default is not inspect._empty: params[name] = param.default if not callable(callback): raise ValueError( f"`callback` should be callable, but `callable({callback})` returns False" ) self.callback = callback if not isinstance(params, (collections.abc.Mapping, Params)): raise TypeError( f"`params` should be a `Mapping` or `Params` but type was: {type(params)}" ) if isinstance(params, Params): # coerce to dict params = {**params} signature = inspect.signature(callback) if not all([param in signature.parameters for param in params]): invalid_params = [ param for param in params if param not in signature.parameters ] raise ValueError( f"Invalid params for callback: {invalid_params}\n" f"Callback parameters are: {signature.parameters}" ) self.params = params
def __repr__(self): return f"FeatureExtractor(callback={self.callback.__qualname__}, params={self.params})"
[docs] def make( self, sound: Sound | AudioFile | Sequence[Sound | AudioFile], parallelize: bool = True, ) -> Spectrogram | List[Spectrogram]: """Make spectrogram(s) from audio. Makes the spectrograms with `self.callback` using the parameters `self.params`. Takes as input :class:`vocalpy.Sound` or :class:`vocalpy.AudioFile`, or a sequence of either and returns either a :class:`vocalpy.Spectrogram` (given a single :class:`vocalpy.Sound` or :class:`vocalpy.AudioFile` instance) or a list of :class:`vocalpy.Spectrogram` instances (given a sequence). Parameters ---------- sound: vocalpy.Sound, vocalpy.AudioFile, or a sequence of either Source of audio used to make spectrograms. Returns ------- spectrogram : vocalpy.Spectrogram or list of vocalpy.Spectrogram """ validate_sound(sound) # define nested function so vars are in scope and ``dask`` can call it def _to_spect(sound_): """Make a ``Spectrogram`` from an ``Sound`` instance, using self.callback""" if isinstance(sound_, AudioFile): sound_ = Sound.read(sound_.path) spect = self.callback(sound_, **self.params) return spect if isinstance(sound, (Sound, AudioFile)): return _to_spect(sound) spects = [] for sound_ in sound: if parallelize: spects.append(dask.delayed(_to_spect)(sound_)) else: spects.append(_to_spect(sound_)) if parallelize: graph = dask.delayed()(spects) with dask.diagnostics.ProgressBar(): return graph.compute() else: return spects