Source code for pyfar.utils

"""
The utilities contain functions that are helpful when working with multiple
pyfar audio objects. The pyfar gallery gives background information to
:ref:`work with audio objects </gallery/interactive/pyfar_audio_objects.ipynb>`
including an introduction to the channel shape (`cshape`), channel axis
(`caxis`), and channel dimension (`cdim`).
"""
import pyfar as pf
import numpy as np


[docs] def broadcast_cshape(signal, cshape): """ Broadcast a signal to a certain cshape. The channel shape (`cshape`) gives the shape of the audio data excluding the last dimension, which is ``n_samples`` for time domain objects and ``n_bins`` for frequency domain objects. The broadcasting follows the :doc:`numpy broadcasting rules <numpy:user/basics.broadcasting>`. Parameters ---------- signal : Signal, TimeData, FrequencyData The signal to be broadcasted. cshape : tuple The cshape to which `signal` is broadcasted. Returns ------- signal : Signal, TimeData, FrequencyData Broadcasted copy of the input signal """ if not isinstance(signal, (pf.Signal, pf.TimeData, pf.FrequencyData)): raise TypeError("Input data must be a pyfar audio object") signal = signal.copy() signal._data = np.broadcast_to( signal._data, cshape + (signal._data.shape[-1], )) return signal
[docs] def broadcast_cshapes(signals, cshape=None): """ Broadcast multiple signals to a common cshape. The channel shape (`cshape`) gives the shape of the audio data excluding the last dimension, which is ``n_samples`` for time domain objects and ``n_bins`` for frequency domain objects. The broadcasting follows the :doc:`numpy broadcasting rules <numpy:user/basics.broadcasting>`. Parameters ---------- signals : tuple of Signal, TimeData, FrequencyData The signals to be broadcasted in a tuple. cshape : tuple, optional The cshape to which the signals are broadcasted. If `cshape` is ``None`` it is determined from the cshapes of the input signals using :py:func:`numpy:numpy.broadcast_shapes`. The default is ``None``. Returns ------- signals : tuple of Signal, TimeData, FrequencyData The broadcasted copies of the input signals in a tuple. """ for signal in signals: if not isinstance(signal, (pf.Signal, pf.TimeData, pf.FrequencyData)): raise TypeError("All input data must be pyfar audio objects") if cshape is None: cshape = np.broadcast_shapes(*[s.cshape for s in signals]) return [broadcast_cshape(s, cshape) for s in signals]
[docs] def broadcast_cdim(signal, cdim): """ Broadcast a signal to a certain cdim. The channel dimension (`cdim`) gives the dimension of the audio data excluding the last dimension, which is ``n_samples`` for time domain objects and ``n_bins`` for frequency domain objects. The signal is broadcasted to `cdim` by prepending ``cdim - len(signal.cshape)`` dimensions. Parameters ---------- signal : Signal, TimeData, FrequencyData The signal to be broadcasted. cdim : int The cdim to which `signal` is broadcasted. Returns ------- signal : Signal, TimeData, FrequencyData The broadcasted copy input signal """ if not isinstance(signal, (pf.Signal, pf.TimeData, pf.FrequencyData)): raise TypeError("Input data must be a pyfar audio object") if len(signal.cshape) > cdim: raise ValueError( "Can not broadcast: Current channel dimensions exceeds cdim") signal = signal.copy() while len(signal.cshape) < cdim: signal._data = signal._data[None, ...] return signal
[docs] def broadcast_cdims(signals, cdim=None): """ Broadcast multiple signals to a common cdim. The channel dimension (`cdim`) gives the dimension of the audio data excluding the last dimension, which is ``n_samples`` for time domain objects and ``n_bins`` for frequency domain objects. The signals are broadcasted to `cdim` by prepending ``cdim - len(signal.cshape)`` dimensions. Parameters ---------- signals : tuple of Signal, TimeData, FrequencyData The signals to be broadcasted in a tuple. cdim : int, optional The cdim to which `signal` is broadcasted. If `cdim` is ``None`` the signals are broadcasted to the largest `cdim`. The default is ``None``. Returns ------- signals : tuple of Signal, TimeData, FrequencyData The broadcasted copies of the input signals in a tuple. """ for signal in signals: if not isinstance(signal, (pf.Signal, pf.TimeData, pf.FrequencyData)): raise TypeError("All input data must be pyfar audio objects") if cdim is None: cdim = np.max([len(s.cshape) for s in signals]) return [broadcast_cdim(s, cdim) for s in signals]
[docs] def concatenate_channels(signals, caxis=0, broadcasting=False): """ Merge multiple Signal, Timedata or Frequencydata objects along a given caxis. Parameters ---------- signals : tuple of Signal, TimeData or FrequencyData The signals to concatenate. All signals must be of the same object type and either have the same cshape or be broadcastable to the same cshape, except in the dimension corresponding to caxis (the first, by default). If this is the case, set ``broadcasting=True``. caxis : int The channel axis (`caxis`) along which the signals are concatenated. The channel axis gives the axe of the audio data excluding the last dimension, which is ``n_samples`` for time domain objects and ``n_bins`` for frequency domain objects. The default is ``0``. broadcasting: bool If this is ``True``, the signals will be broadcasted to common cshape, except for the caxis along which the signals are concatenated. The caxis of the signals are broadcasted following the :doc:`numpy broadcasting rules <numpy:user/basics.broadcasting>` The default is ``False``. Returns ------- merged : Signal, TimeData, FrequencyData The merged signal object. """ # check input for signal in signals: if not isinstance(signal, (pf.Signal, pf.TimeData, pf.FrequencyData)): raise TypeError("All input data must be of type pf.Signal, " "pf.TimeData or pf.FrequencyData.") if not isinstance(broadcasting, bool): raise TypeError("'broadcasting' needs to be False or True.") # check matching meta data of input signals. [signals[0]._assert_matching_meta_data(s) for s in signals] # check if any signal is complex valued is_result_complex = False for s in signals: if (type(s) is not pf.FrequencyData) and s.complex: is_result_complex = True # broadcast signals into largest dimension and common cshapes if broadcasting is True: # broadcast signals into common cshape cshapes = [s.cshape for s in signals] max_cdim = np.max([len(sh) for sh in cshapes]) for i, sh in enumerate(cshapes): for _ in range(max_cdim-len(sh)): cshapes[i] = (1,) + cshapes[i] # Finds broadcast cshape without the caxis to ignore cshape_bc = np.broadcast_shapes(*np.delete(cshapes, caxis, axis=-1)) broad_signals = [] for signal, cshape in zip(signals, cshapes): # Appends the caxis to ignore back into cshape to broadcast to. if caxis in (-1, len(cshape_bc)): # Use append if caxis is defined for last dimension cs = np.append(cshape_bc, cshape[caxis]) else: # Use insert if caxis is not defined for last dim axis = caxis+1 if caxis < 0 else caxis cs = np.insert(cshape_bc, axis, cshape[caxis]) broad_signals.append(broadcast_cshape(signal, tuple(cs))) signals = broad_signals # merge the signals along axis axis = caxis-1 if caxis < 0 else caxis # distinct by type using s._data does not work # - shapes of data would not match in mixed domain concatenation # - fft_norm may cause wrong time domain amplitudes in frequency domain # concatenation if type(signal) is pf.FrequencyData: data = np.concatenate([s.freq for s in signals], axis=axis) else: data = np.concatenate([s.time for s in signals], axis=axis) # return merged Signal if isinstance(signals[0], pf.Signal): return pf.Signal(data, signals[0].sampling_rate, n_samples=signals[0].n_samples, domain=signals[0].domain, fft_norm=signals[0].fft_norm, is_complex=is_result_complex) elif isinstance(signals[0], pf.TimeData): return pf.TimeData(data, signals[0].times, is_complex=is_result_complex) else: return pf.FrequencyData(data, signals[0].frequencies)