Source code for strauss.utilities

""" The :obj:`utilities` submodule: useful functions for ``strauss``

This submodule is for useful utility functions used by other
``strauss`` modules. Generally these are not intended for direct
use by the user.
"""
from functools import reduce
import operator
import numpy as np
from scipy.interpolate import interp1d
from contextlib import contextmanager,redirect_stderr,redirect_stdout
from os import devnull
from io import StringIO 
import sys
from pathlib import Path

# Some utility classes (these may graduate to somewhere else eventually)

[docs] class NoSoundDevice: """ Drop-in replacement for sounddevice module if not working, so we can still use other functionality. Attributes: err (:obj:`Exception`): Error message from trying to import sounddevice """ def __init__(self, err): self.err = err
[docs] def play(self, *args, **kwargs): """Dummy function replacing `sounddevice.play` when unavailable. Args: *args: arguments (ignored) **kwargs: keyword-only arguments (ignored) """ raise self.err
class Equaliser: def __init__(self): self.factor_rms = None parpath = Path(f"{Path(__file__).parent}","data","params.csv") # Read in parameters for ISO 226:2024 standard. pars = np.genfromtxt(parpath, delimiter=',', names=True) self.parfuncs = {} for c in pars.dtype.names: if c == 'freq': continue self.parfuncs[c] = interp1d(np.log10(pars['freq']), pars[c], fill_value='extrapolate') def get_relative_loudness_norm(self, freq, phon=70.): """ Relative normalisation of sound frequencies To compensate for pereceptual loudness, following the ISO 226:2024 standard. Args: freq (:obj:`array-like`) audio frequencies in Hz phon (:obj:`float`) listening level for a 1 kHz note Returns: rnorm (:obj:`array-like`) volume normalisation for spectra """ lfreq = np.log10(freq) L_U = self.parfuncs['L_U'](lfreq) alpha_f = self.parfuncs['alpha_f'](lfreq) T_f = self.parfuncs['T_f'](lfreq) A = pow(4e-10, 0.3 - alpha_f) B = pow(10., (phon)*3e-2) - pow(10, 7.2e-2) C = pow(10., alpha_f * 0.1*(T_f + L_U)) L_f = 10*np.log10(A*B + C)/alpha_f - L_U norm = pow(10., (L_f - phon)/20) rnorm = norm/norm.max() return rnorm # a load of utility functions used by STRAUSS
[docs] def nested_dict_reassign(fromdict, todict): """ Recurse through dictionaries and sub-dictionaries in `fromdict` and reassign equivalent values in `todict` Args fromdict (:obj:`dict`): Dictionary containing values to assign todict (:obj:`dict`): Dictionary containing values to be reassigned """ for k, v in fromdict.items(): if isinstance(v, dict): # recurse through nested dictionaries nested_dict_reassign(v, todict[k]) else: # reassign todict value todict[k] = v
[docs] def nested_dict_fill(fromdict, todict): """ Recurse through dictionaries and sub-dictionaries in `fromdict` and assign to any entries missing from `todict` Args: fromdict (:obj:`dict`): Dictionary containing values to assign todict (:obj:`dict`): Dictionary containing values to be reassigned """ for k, v in fromdict.items(): if k not in todict: # assign todict value todict[k] = v elif isinstance(v, dict): # recurse through nested dictionaries nested_dict_fill(todict[k], v)
[docs] def nested_dict_idx_reassign(fromdict, todict, idx): """ Recurse through dictionaries and sub-dictionaries of iterables in `fromdict` and index value idx to assign or replact value in todict Args: fromdict (:obj:`dict`): Dictionary containing values to assign todict (:obj:`dict`): Dictionary containing values to be reassigned idx (:obj:`dict`): Index value for retrieving value from iterables """ for k, v in fromdict.items(): if isinstance(v, dict): # recurse through nested dictionaries nested_dict_idx_reassign(todict[k], v, idx) else: # reassign todict value todict[k] = v[idx]
[docs] def reassign_nested_item_from_keypath(dictionary, keypath, value): """ Reassign item in a nested dictionary to value using keypath syntax, to traverse multiple dictionaries Args: dictionary (:obj:`dict`): dict object to reassign values within keypath (:obj:`str`): Using filepath syntax on given OS to traverse dictionary, i.e 'a/b/c' ('a\\b\\c') corresponds to dict['a']['b']['c'] on Unix (Windows). value: value to reassign dictionary value with """ p = Path(keypath) keylist = list(p.parts) get_item = lambda d, kl: reduce(operator.getitem, kl, d) get_item(dictionary, keylist[:-1])[keylist[-1]] = value
[docs] def linear_to_nested_dict_reassign(fromdict, todict): """ Iterate through a linear dictionary to reassign nested values using keypaths (d1['a/b/c'] -> d2['a']['b']['c'], d1['a']->d2['a']) Args: fromdict (:obj:`dict`): Dictionary containing values to assign todict (:obj:`dict`): Dictionary containing values to be reassigned """ for k, v in fromdict.items(): reassign_nested_item_from_keypath(todict, k, v)
[docs] def const_or_evo_func(x): """ If x is callable, return x, else provide a function that just returns x Args: x: input value, either a numerical value or a function """ if callable(x): return x else: return lambda y: y*0 + x
[docs] def const_or_evo(x,t): """ If x is callable, return x(t), else return x Args: x: input value, either a numerical value or a function t (numerical): values to evaluate x function """ if callable(x): return x(t) else: return x
[docs] def rescale_values(x, oldlims, newlims): """ Rescale x values defined by limits oldlims to new limits newlims Args: x (array-like): Array of input values oldlims (:obj:`tuple`): tuple representing the original limits of `x` (low, high) newlims (:obj:`tuple`): tuple representing the new limits Returns: x_rs (array-like): Rescaled array """ olo, ohi = oldlims nlo, nhi = newlims descale = np.clip((x - olo) / (ohi-olo), 0 , 1) return (nhi-nlo)*descale + nlo
[docs] def resample(rate_in, samprate, wavobj): """ Resample audio from original samplerate to required samplerate Args: rate_in (:obj:`int`) sample rate of input wave object samprate (:obj:`int`) desired sample rate for output wavobj (:obj:`tuple`) sample rate, sample array tuple, output by `scipy.io.wavfile` function Returns: new_wavobj (:obj:`tuple`) as `wavobj`, with new sample rate and resampled sample values """ duration = wavobj.shape[0] / rate_in time_old = np.linspace(0, duration, wavobj.shape[0]) time_new = np.linspace(0, duration, int(wavobj.shape[0] * samprate / rate_in)) interpolator = interp1d(time_old, wavobj.T) new_wavobj = np.round(interpolator(time_new).T).astype(wavobj.dtype) return(new_wavobj)
[docs] @contextmanager def suppress_stdout_stderr(): """ A context manager that redirects stdout and stderr to devnull """ with open(devnull, 'w') as fnull: with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out: yield (err, out)
[docs] def get_supported_coqui_voices(): """Show supported Coqui-AI TTS voices """ voices = [] jenny = {"id": Path('tts_models', 'en', 'jenny', 'jenny'), "name": 'jenny', "languages": ['en_GB'], "age": None} voices.append(jenny) vits = {"id": Path('tts_models', '{iso_code}', 'fairseq', 'vits'), "name": 'fairseq-{iso_code}', "languages": ['many (https://dl.fbaipublicfiles.com/mms/tts/all-tts-languages.html for iso codes)'], "age": None} voices.append(vits) vits_ita = {"id": Path('tts_models', 'it', 'mai_female', 'glow-tts'), "name": 'mai', "languages": ['it_IT'], "age": None} voices.append(vits_ita) thorsten = {"id": Path('tts_models', 'de', 'thorsten', 'vits'), "name": 'thorsten', "languages": ['de_DE'], "age": None} voices.append(thorsten) return voices
[docs] class Capturing(list): """ Context manager for handling stdout (see https://stackoverflow.com/a/16571630) """ def __enter__(self): self._stdout = sys.stdout sys.stdout = self._stringio = StringIO() return self def __exit__(self, *args): self.extend(self._stringio.getvalue().splitlines()) del self._stringio # free up some memory sys.stdout = self._stdout