Source code for sparrowpy.sound_object

"""SoundObject class for spatial audio reproduction."""
import matplotlib
import numpy as np
import pyfar as pf
import sofar as sf


class _DirectivityMS():
    """Directivity class for FreeFieldDirectivityTF convention."""

    data: pf.FrequencyData
    receivers: pf.Coordinates

    def __init__(self, file_path: str, source_index=0) -> None:
        """Init DirectivityMS.

        Parameters
        ----------
        file_path : str
            directivity path for sofa file.
        source_index : int, optional
            source index of directivity, by default 0

        """
        sofa = sf.read_sofa(file_path)
        if sofa.GLOBAL_SOFAConventions != 'FreeFieldDirectivityTF':
            raise ValueError('convention need to be FreeFieldDirectivityTF')
        sofa = sf.read_sofa(file_path)
        self.data = pf.FrequencyData(
            sofa.Data_Real[source_index, :] + 1j * sofa.Data_Imag[
                source_index, :], sofa.N)
        if sofa.ReceiverPosition_Type == 'spherical':
            pos = sofa.ReceiverPosition.squeeze().T
            pos[0] = (pos[0] + 360) % 360
            self.receivers = pf.Coordinates(
                pos[0], pos[1], pos[2], 'sph', 'top_elev', 'deg')

    def get_directivity(
            self, source_pos: np.ndarray, source_view: np.ndarray,
            source_up: np.ndarray, target_position: np.ndarray,
            i_freq: int) -> float:
        """Get Directivity for certain position.

        Parameters
        ----------
        source_pos : np.ndarray
            cartesian source position in m
        source_view : np.ndarray
            cartesian source view in m
        source_up : np.ndarray
            cartesian source up in m
        target_position : np.ndarray
            cartesian target position in m
        i_freq : int
            frequency bin index

        Returns
        -------
        float
            nearest directivity factor for given position and orientation.

        """
        (azimuth_deg, elevation_deg) = _get_metrics(
            source_pos, source_view, source_up, target_position)
        index, _ = self.receivers.find_nearest_k(
            (azimuth_deg+360) % 360, elevation_deg, 1, k=1,
            domain='sph', convention='top_elev', unit='deg')
        return self.data.freq[index, i_freq]


def _get_metrics(pos_G, view_G, up_G, target_pos_G):
    pos_G = np.array(pos_G, dtype=float)
    view_G = np.array(view_G, dtype=float)
    up_G = np.array(up_G, dtype=float)
    target_pos_G = np.array(target_pos_G, dtype=float)
    direction_G = target_pos_G - pos_G

    x_dash = np.cross(view_G, up_G)
    y_dash = up_G
    z_dash = -view_G

    w_x_local = np.dot(direction_G, x_dash)
    w_y_local = np.dot(direction_G, y_dash)
    w_z_local = np.dot(direction_G, z_dash)

    azimuth_deg = np.arctan2(-w_x_local, -w_z_local) / np.pi * 180
    w = np.array([w_x_local, w_y_local, w_z_local])
    w_y_local_normalized = w_y_local / np.sqrt(np.dot(w, w))
    elevation_deg = np.arcsin(w_y_local_normalized) / np.pi * 180

    return (azimuth_deg, elevation_deg)


[docs] class SoundObject(): """A class holding the common properties for Source and Receiver.""" position: np.ndarray view: np.ndarray up: np.ndarray def __init__( self, position: np.ndarray, view: np.ndarray, up: np.ndarray) -> None: """Init a sound object. Parameters ---------- position : np.ndarray position of the sound object in m view : np.ndarray view vector of sound object up : np.ndarray uo vector of sound object """ self.position = np.array(position, dtype=float) assert self.position.shape == (3,) self.view = np.array(view, dtype=float) self.view /= np.sqrt(np.dot(view, view)) assert self.view.shape == (3,) self.up = np.array(up, dtype=float) self.up /= np.sqrt(np.dot(up, up)) assert self.up.shape == (3,)
[docs] def plot(self, ax: matplotlib.axes.Axes, **kwargs): """Plot SoundObject position and orientation. Parameters ---------- ax : matplotlib.axes.Axes Axes to plot on. **kwargs Keyword arguments that are passed to ``matplotlib.pyplot.scatter()``. """ xyz = self.position ax.scatter(xyz[0], xyz[1], xyz[2], kwargs)
[docs] class SoundSource(SoundObject): """Acoustic sound source inhered from SoundObject.""" directivity: _DirectivityMS sound_power: float def __init__( self, position: np.ndarray, view: np.ndarray, up: np.ndarray, directivity: _DirectivityMS = None, sound_power: float = 1) -> None: """Init sound source. Parameters ---------- position : np.ndarray position of the sound source in m view : np.ndarray view vector of sound source up : np.ndarray uo vector of sound source directivity : DirectivityMS, optional Directivity, by default None sound_power : float, optional sound power of the source in Watt, by default 1 """ super(SoundSource, self).__init__(position, view, up) self.sound_power = float(sound_power) if directivity is not None: assert isinstance(directivity, _DirectivityMS) self.directivity = directivity
[docs] def plot(self, ax, **kwargs): """Plot Source position and orientation. Parameters ---------- ax : matplotlib.axes.Axes Axes to plot on. **kwargs Keyword arguments that are passed to ``matplotlib.pyplot.scatter()``. """ super(SoundSource, self).plot(ax, color='r', label='Source', **kwargs)
[docs] class Receiver(SoundObject): """Receiver object inhered from SoundObject.""" def __init__( self, position: np.ndarray, view: np.ndarray, up: np.ndarray) -> None: """Init sound receiver. Parameters ---------- position : np.ndarray cartesian positions for receiver. view : np.ndarray view vector of sound receiver. up : np.ndarray up vector of sound receiver. """ super(Receiver, self).__init__(position, view, up)
[docs] def plot(self, ax, **kwargs): """Plot Receiver position and orientation. Parameters ---------- ax : matplotlib.axes.Axes Axes to plot on. **kwargs Keyword arguments that are passed to ``matplotlib.pyplot.scatter()``. """ super(Receiver, self).plot(ax, color='b', label='Receiver', **kwargs)