示例#1
0
文件: utils.py 项目: nidhisawant/masp
def lagrange(N, delays):
    """
    Design a fractional delay order-N filter matrix with polynomial interpolation.

    Parameters
    ----------
    N : int
        Filter order.
    delays : ndarray
       Target fractional delays, in samples. Dimension = (1).

    Returns
    -------
    h : ndarray
        Target filter. Dimension = (N+1, len(delays))

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    For best results, delay should be near N/2 +/- 1.
    """

    _validate_int('N', N, positive=True)
    _validate_ndarray_1D('delays', delays, positive=True)

    n = np.arange(N + 1)
    h = np.ones((N + 1, delays.size))
    for l in range(delays.size):
        for k in range(N + 1):
            idx = n[n != k]
            h[idx, l] = h[idx, l] * (delays[l] - k) / (n[idx] - k)
    return h
示例#2
0
def dsph_hankel1(n, x):
    """
    Spherical hankel function derivative of the first kind.

    Parameters
    ----------
    n : int
        Function order.
    x: ndarray
        Points where to evaluate the function. Dimension = (l)

    Returns
    -------
    f : ndarray
        Function result. Dimension = (l)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    """
    _validate_int('n', n)
    _validate_ndarray_1D('x', x)
    return spherical_jn(
        n, x, derivative=True) + 1j * spherical_yn(n, x, derivative=True)
示例#3
0
def rec_module_sh(echograms, sh_orders):
    """
    Apply spherical harmonic directivity gains to a set of given echograms.

    Parameters
    ----------
    echograms : ndarray, dtype = Echogram
        Target echograms. Dimension = (nSrc, nRec)
    sh_orders : int or ndarray, dtype = int
        Spherical harmonic expansion order. Dimension = 1 or (nRec)

    Returns
    -------
    rec_echograms : ndarray, dtype = Echogram
        Echograms subjected to microphone gains. Dimension = (nSrc, nRec)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    If `sh_orders` is an integer, the given order will be applied to all receivers.

    """

    nSrc = echograms.shape[0]
    nRec = echograms.shape[1]
    _validate_echogram_array(echograms)
    if isinstance(sh_orders, int):
        _validate_int('sh_orders', sh_orders, positive=True)
        sh_orders = sh_orders * np.ones(nRec)
    else:
        _validate_ndarray_1D('sh_orders',
                             sh_orders,
                             size=nRec,
                             positive=True,
                             dtype=int)

    rec_echograms = copy.copy(echograms)
    # Do nothing if all orders are zeros(omnis)
    if not np.all(sh_orders == 0):
        for ns in range(nSrc):
            for nr in range(nRec):
                # Get vectors from source to receiver
                sph = cart2sph(echograms[ns, nr].coords)
                azi = sph[:, 0]
                polar = np.pi / 2 - sph[:, 1]

                sh_gains = get_sh(int(sh_orders[nr]),
                                  np.asarray([azi, polar]).transpose(), 'real')
                print(sh_gains)
                # rec_echograms[ns, nr].value = sh_gains * echograms[ns, nr].value[:,np.newaxis]
                rec_echograms[ns,
                              nr].value = sh_gains * echograms[ns, nr].value

    _validate_echogram_array(rec_echograms)
    return rec_echograms
示例#4
0
文件: utils.py 项目: nidhisawant/masp
def check_cond_number_sht(N, dirs, basisType, W=None):
    """
    Computes the condition number for a least-squares SHT.

    Parameters
    ----------
    N : int
        Maximum order to be tested for the given set of points.
   dirs : ndarray
        Evaluation directions. Dimension = (nDirs, 2).
        Directions are expected in radians, expressed in pairs [azimuth, inclination].
    basisType : str
        Type of spherical harmonics. Either 'complex' or 'real'.
    W : ndarray, optional.
        Weights for each measurement point to condition the inversion,
        in case of a weighted least-squares. Dimension = (nDirs)

    Returns
    -------
    cond_N : ndarray
        Condition number for each order. Dimension = (N+1)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    Inclination is defined as the angle from zenith: inclination = pi/2-elevation

    TODO: implement complex basis?
    TODO: implement W
    """

    _validate_int('N', N, positive=True)
    _validate_ndarray_2D('dirs', dirs, shape1=C - 1)
    _validate_string('basisType', basisType, choices=['complex', 'real'])
    if W is not None:
        _validate_ndarray_1D('W', W, size=dirs.shape[0])

    # Compute the harmonic coefficients
    Y_N = get_sh(N, dirs, basisType)

    # Compute condition number for progressively increasing order up to N
    cond_N = np.zeros(N + 1)
    for n in range(N + 1):
        Y_n = Y_N[:, :np.power(n + 1, 2)]
        if W is None:
            YY_n = np.dot(Y_n.T, Y_n)
        else:
            # YY_n = Y_n.T * np.diag(W) * Y_n
            raise NotImplementedError
        cond_N[n] = np.linalg.cond(YY_n)

    return cond_N
示例#5
0
def cyl_modal_coefs(N, kr, arrayType):
    """
    Modal coefficients for rigid or open cylindrical array

    Parameters
    ----------
    N : int
        Maximum spherical harmonic expansion order.
    kr: ndarray
        Wavenumber-radius product. Dimension = (l).
    arrayType: str
        'open' or 'rigid'.

    Returns
    -------
    b_N : ndarray
        Modal coefficients. Dimension = (l, N+1)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    The `arrayType` options are:
    - 'open' for open array of omnidirectional sensors
    - 'rigid' for sensors mounted on a rigid baffle.

    """

    _validate_int('N', N)
    _validate_ndarray_1D('kr', kr, positive=True)
    _validate_string('arrayType', arrayType, choices=['open', 'rigid'])

    b_N = np.zeros((kr.size, N + 1), dtype='complex')

    for n in range(N + 1):

        if arrayType is 'open':
            b_N[:, n] = np.power(1j, n) * jv(n, kr)

        elif arrayType is 'rigid':
            jn = jv(n, kr)
            jnprime = jvp(n, kr, 1)
            hn = hankel2(n, kr)
            hnprime = h2vp(n, kr, 1)

            temp = np.power(1j, n) * (jn - (jnprime / hnprime) * hn)
            temp[np.where(kr == 0)] = 1 if n == 0 else 0.
            b_N[:, n] = temp

    return b_N
示例#6
0
def get_rt_sabine(alpha, room, abs_wall_ratios):
    """
    Estimate RT60 through Sabine's method.

    Parameters
    ----------
    alpha: int, float or 1-D ndarray
        Absorption coefficient.
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    abs_wall_ratios : ndarray
        Wall absorption coefficients, in the range [0,1]. Dimension = (6).

    Returns
    -------
    rt60 : float
        Estimated reverberation time.

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    As opposed to `find_abs_coeffs_from_rt()`, `abs_wall_ratios` must be explicit.

    `abs_wall_ratios` must have all values in the range [0,1].

    """

    # Validate arguments
    _validate_number('alpha', alpha)
    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_1D('abs_wall_ratios', abs_wall_ratios, size=2*C, norm=True)

    l, w, h = room
    V = l*w*h   # room volume
    Stot = 2 * ( (l * w) + (l * h) + (w * h) ) # room area

    alpha_walls = alpha * abs_wall_ratios
    a_x = alpha_walls[[0,1]]
    a_y = alpha_walls[[2,3]]
    a_z = alpha_walls[[4,5]]

    # Mean absorption
    a_mean = np.sum( (w * h * a_x) + (l * h * a_y) + (l * w * a_z) ) / Stot
    rt60 = (55.25 * V) / ( c * Stot * a_mean )

    return rt60
示例#7
0
def quantise_echogram(echogram, nGrid, echo2gridMap):
    """
    Quantise the echogram reflections to specific rendering directions.

    Parameters
    ----------
    echogram : Echogram
        Target Echogram
    nGrid: int
        Number of grid points where to render reflections.
    echo2gridMap: ndarray, dtype: int
        Mapping between echgram and grid points, as generated by `get_echo2gridMap()`

    Returns
    -------
    q_echogram : ndarray, dtype: QuantisedEchogram
        Quantised echograms. Dimension = (nGrid)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    """

    _validate_echogram(echogram)
    _validate_int('nGrid', nGrid)
    _validate_ndarray_1D('echo2gridMap', echo2gridMap, positive=True, dtype=int)

    # Initialise echogram structure with each quantised echogram
    # Adjust array length to the common minimum so that there is no index overflow
    if np.size(echo2gridMap) > np.size(echogram.time):
        echo2gridMap = echo2gridMap[:np.size(echogram.time)]
    elif np.size(echo2gridMap) < np.size(echogram.time):
        echogram.value = echogram.value[:np.size(echo2gridMap)]
        echogram.time = echogram.time[:np.size(echo2gridMap)]

    q_echogram = np.empty(nGrid, dtype=QuantisedEchogram)
    for n in range(nGrid):
        value = echogram.value[echo2gridMap == n]
        # print(len(echogram.value[echo2gridMap == n]))
        time = echogram.time[echo2gridMap == n]
        isActive = False if np.size(time) == 0 else True
        q_echogram[n] = QuantisedEchogram(value, time, isActive)
    return q_echogram
示例#8
0
文件: utils.py 项目: nidhisawant/masp
def replicate_per_order(x):
    """
    Replicate l^th element 2*l+1 times across dimension

    Parameters
    ----------
    x : ndarray
        Array to replicate. Dimension = (l)

    Returns
    -------
    x_rep : ndarray
        Replicated array. Dimension = ( (l+1)^2 )

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    Replicates multidimensional array across dimension dim, so that the
    l^th element of dim is replicated 2*l+1 times. that effectively has the
    effect that the dimension grows from L to L^2 elements. This can be useful
    in some spherical harmonic operations.

    TODO: FOR THE MOMENT JUST 1D
    TODO: optimize
    """

    _validate_ndarray_1D('x', x)

    order = np.size(x) - 1
    n_sh = np.power(order + 1, 2)
    x_rep = np.zeros(n_sh, dtype=x.dtype)
    sh_idx = 0

    for m in range(order + 1):
        n_sh_order = 2 * m + 1  # number of spherical harmonics at the given order m
        for n in range(n_sh_order):
            x_rep[sh_idx] = x[m]
            sh_idx += 1

    return x_rep
示例#9
0
文件: utils.py 项目: nidhisawant/masp
def sph2cart(sph):
    """
    Spherical to cartesian coordinates transformation, in matrix form.

    Parameters
    ----------
    sph : ndarray
        Spherical coordinates, in radians, aed.  Dimension = (nCoords, C)


    Returns
    -------
    sph : ndarray
        Cartesian coordinates. Dimension = (nCoords, C)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    As a dimensionality exception, in case the input matrix is 1D (just one point),
    the output matrix will be as well 1D.
    """

    arg = sph.copy()
    _validate_ndarray('sph', sph)
    if sph.ndim == 1:
        _validate_ndarray_1D('sph', sph, size=C)
        sph = sph[np.newaxis, :]
    elif sph.ndim == 2:
        _validate_ndarray_2D('sph', sph, shape1=C)
    else:
        raise ValueError('sph must be either 1D or 2D array')

    cart = np.empty(sph.shape)
    cart[:, 2] = sph[:, 2] * np.sin(sph[:, 1])
    rcoselev = sph[:, 2] * np.cos(sph[:, 1])
    cart[:, 0] = rcoselev * np.cos(sph[:, 0])
    cart[:, 1] = rcoselev * np.sin(sph[:, 0])
    if arg.ndim == 1:
        cart = cart.squeeze()
    return cart
示例#10
0
文件: utils.py 项目: nidhisawant/masp
def cart2sph(cart):
    """
    Cartesian to spherical coordinates transformation, in matrix form.

    Parameters
    ----------
    cart : ndarray
      Cartesian coordinates. Dimension = (nCoords, C)

    Returns
    -------
    sph : ndarray
        Spherical coordinates, in radians, aed.  Dimension = (nCoords, C)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    As a dimensionality exception, in case the input matrix is 1D (just one point),
    the output matrix will be as well 1D.
    """

    arg = cart.copy()
    _validate_ndarray('cart', cart)
    if cart.ndim == 1:
        _validate_ndarray_1D('cart', cart, size=C)
        cart = cart[np.newaxis, :]
    elif cart.ndim == 2:
        _validate_ndarray_2D('cart', cart, shape1=C)
    else:
        raise ValueError('cart must be either 1D or 2D array')

    sph = np.empty(cart.shape)
    hypotxy = np.hypot(cart[:, 0], cart[:, 1])
    sph[:, 2] = np.hypot(hypotxy, cart[:, 2])
    sph[:, 1] = np.arctan2(cart[:, 2], hypotxy)
    sph[:, 0] = np.arctan2(cart[:, 1], cart[:, 0])
    if arg.ndim == 1:
        sph = sph.squeeze()
    return sph
示例#11
0
def ims_coreMtx(room, source, receiver, type, typeValue):
    """
    Compute echogram by image source method.

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    src : ndarray
        Source position in cartesian coordinates. Dimension = (3) [x, y, z].
    rec : ndarray
        Receiver position in cartesian coordinates. Dimension = (3) [x, y, z].
    type : str
        Restriction type: 'maxTime' or 'maxOrder'
    typeValue: int or float
        Value of the chosen restriction.

    Returns
    -------
    reflections : echogram
        An Echogram instance.

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `src` and `rec` positions are specified from the left ground corner
    of the room, using a left-handed coordinate system.
    `room` refers to the wall dimensions.
    Therefore, their values should be positive and smaller than room dimensions.

              _____    _
             |     |   |
             |     |   |
           x ^     |   | l = r[0]
             |     |   |
             |     |   |
             o---->    -
                  y
             |-----|
                w = r[1]

    """

    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_1D('source', source, size=C, positive=True, limit=[np.zeros(C),room])
    _validate_ndarray_1D('receiver', receiver, size=C, positive=True, limit=[np.zeros(C),room])
    _validate_string('type', type, choices=['maxTime', 'maxOrder'])

    # Room dimensions
    l, w, h = room

    # Move source origin to the centrer of the room
    src = np.empty(C)
    src[0] = source[0] - l / 2
    src[1] = w / 2 - source[1]
    src[2] = source[2] - h / 2

    # Move receiver origin to the centrer of the room
    rec = np.empty(C)
    rec[0] = receiver[0] - l / 2
    rec[1] = w / 2 - receiver[1]
    rec[2] = receiver[2] - h / 2

    if type is 'maxOrder':
        maxOrder = typeValue
        echogram = ims_coreN(room, src, rec, maxOrder)
    elif type is 'maxTime':
        maxDelay = typeValue
        echogram = ims_coreT(room, src, rec, maxDelay)

    # Sort reflections according to propagation time
    idx = np.argsort(echogram.time)
    echogram.time = echogram.time[idx]
    echogram.value = echogram.value[idx]
    echogram.order = echogram.order[idx, :]
    echogram.coords = echogram.coords[idx, :]

    return echogram
示例#12
0
def ims_coreT(room, src, rec, maxTime):
    """
    Compute echogram by image source method, under maxTime restriction

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    src : ndarray
        Source position in cartesian coordinates. Dimension = (3) [x, y, z].
    rec : ndarray
        Receiver position in cartesian coordinates. Dimension = (3) [x, y, z].
    maxTime : float
        Maximum echogram computation time.

    Returns
    -------
    reflections : echogram
        An Echogram instance.

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `src` and `rec` positions are specified from a right-handed coordinate system
     placed at the center of the room, with +x facing front, and +y facing left.
     (as opposite to `ims_coreMtx`).
     However, `room` refer to the wall dimensions.
     Therefore, given values must be in the range +-room[i]/2.

                ^x
              __|__    _
             |  |  |   |
             |  |  |   |
          y<----o  |   | l = r[0]
             |     |   |
             |     |   |
             |_____|   -

             |-----|
                w = r[1]

    """

    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_1D('source', src, size=C, limit=[-room/2,room/2])
    _validate_ndarray_1D('receiver', rec, size=C, limit=[-room/2,room/2])
    _validate_number('maxTime', maxTime, positive=True)

    # Find order N that corresponds to maximum distance
    d_max = maxTime * c
    Nx = np.ceil(d_max / room[0])
    Ny = np.ceil(d_max / room[1])
    Nz = np.ceil(d_max / room[2])

    # i, j, k indices for calculation in x, y, z respectively
    rx = np.arange(-Nx, Nx + 1)
    ry = np.arange(-Ny, Ny + 1)
    rz = np.arange(-Nz, Nz + 1)
    xx, yy, zz = np.meshgrid(rx, ry, rz)
    # Vectorize (transpose idx due to matlab/python variations on matrix handling)
    i = xx.transpose(2,0,1).flatten()
    j = yy.transpose(2,0,1).flatten()
    k = zz.transpose(2,0,1).flatten()
    # Image source coordinates with respect to receiver
    s_x = i*room[0] + np.power(-1.,i)*src[0] - rec[0]
    s_y = j*room[1] + np.power(-1.,j)*src[1] - rec[1]
    s_z = k*room[2] + np.power(-1.,k)*src[2] - rec[2]
    # Distance
    s_d = np.sqrt(np.power(s_x,2) + np.power(s_y,2) + np.power(s_z,2))

    # Bypass image sources with d > dmax
    i = i[s_d < d_max]
    j = j[s_d < d_max]
    k = k[s_d < d_max]
    s_x = s_x[s_d < d_max]
    s_y = s_y[s_d < d_max]
    s_z = s_z[s_d < d_max]
    s_d = s_d[s_d < d_max]

    # Reflection propagation time
    s_t = s_d/c
    # Reflection propagation attenuation - if distance is <1m
    # set at attenuation at 1 to avoid amplification
    s_att = np.zeros(s_d.size)
    s_att[s_d <= 1] = 1
    s_att[s_d > 1] = 1./s_d[s_d > 1]

    # Write to echogram structure
    reflections = Echogram(value=s_att[:, np.newaxis],
                           time=s_t,
                           order=np.asarray(np.stack([i, j, k], axis=1), dtype=int),
                           coords=np.stack([s_x, s_y, s_z], axis=1))

    return reflections
示例#13
0
def ims_coreN(room, src, rec, N):
    """
    Compute echogram by image source method, under reflection order restriction

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    src : ndarray
        Source position in cartesian coordinates. Dimension = (3) [x, y, z].
    rec : ndarray
        Receiver position in cartesian coordinates. Dimension = (3) [x, y, z].
    N : int
        Maximum reflection order.

    Returns
    -------
    reflections : echogram
        An Echogram instance.

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `src` and `rec` positions are specified from a right-handed coordinate system
     placed at the center of the room, with +x facing front, and +y facing left.
     (as opposite to `ims_coreMtx`).
     However, `room` refer to the wall dimensions.
     Therefore, given values must be in the range +-room[i]/2.

                ^x
              __|__    _
             |  |  |   |
             |  |  |   |
          y<----o  |   | l = r[0]
             |     |   |
             |     |   |
             |_____|   -

             |-----|
                w = r[1]

    """

    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_1D('source', src, size=C, limit=[-room/2,room/2])
    _validate_ndarray_1D('receiver', rec, size=C, limit=[-room/2,room/2])
    _validate_int('N', N, positive=True)

    # i,j,k indices for calculation in x,y,z respectively
    r = np.arange(-N, N+1)
    xx, yy, zz = np.meshgrid(r, r, r)
    # Vectorize (kind of empirical...)
    i = zz.reshape(zz.size)
    j = xx.reshape(xx.size)
    k = yy.reshape(yy.size)
    # Compute total order and select only valid incides up to order N
    s_ord = np.abs(i) + np.abs(j) + np.abs(k)
    i = i[s_ord <= N]
    j = j[s_ord <= N]
    k = k[s_ord <= N]

    # Image source coordinates with respect to receiver
    s_x = i*room[0] + np.power(-1.,i)*src[0] - rec[0]
    s_y = j*room[1] + np.power(-1.,j)*src[1] - rec[1]
    s_z = k*room[2] + np.power(-1.,k)*src[2] - rec[2]

    # Distance
    s_d = np.sqrt(np.power(s_x,2) + np.power(s_y,2) + np.power(s_z,2))
    # Reflection propagation time
    s_t = s_d/c
    # Reflection propagation attenuation - if distance is <1m
    # set at attenuation at 1 to avoid amplification
    s_att = np.empty(s_d.size)
    s_att[s_d <= 1] = 1
    s_att[s_d > 1] = 1./s_d[s_d > 1]

    # Write to echogram structure
    reflections = Echogram(value=s_att[:, np.newaxis],
                           time=s_t,
                           order=np.asarray(np.stack([i, j, k], axis=1), dtype=int),
                           coords=np.stack([s_x, s_y, s_z], axis=1))

    return reflections
示例#14
0
def sph_modal_coefs(N, kr, arrayType, dirCoef=None):
    """
    Modal coefficients for rigid or open spherical array

    Parameters
    ----------
    N : int
        Maximum spherical harmonic expansion order.
    kr: ndarray
        Wavenumber-radius product. Dimension = (l).
    arrayType: str
        'open', 'rigid' or 'directional'.
    dirCoef: float, optional
        Directivity coefficient of the sensor. Default to None.

    Returns
    -------
    b_N : ndarray
        Modal coefficients. Dimension = (l, N+1)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    The `arrayType` options are:
    - 'open' for open array of omnidirectional sensors,
    - 'rigid' for sensors mounted on a rigid baffle,
    - 'directional' for an open array of first-order directional microphones determined by `dirCoef`.

    `dirCoef` is relevant (and required) only in the 'directional' type.
    `dirCoef` ranges from 0 (omni) to 1 (dipole), where for example 0.5 is a cardioid sensor.
    In the 0 case it is equivalent to an open array of omnis.
    The first order directivity function is defined as d(theta) = dirCoeff + (1-dirCoeff)*cos(theta).

    """

    _validate_int('N', N, positive=True)
    _validate_ndarray_1D('kr', kr, positive=True)
    _validate_string('arrayType',
                     arrayType,
                     choices=['open', 'rigid', 'directional'])
    if arrayType is 'directional':
        if dirCoef is None:
            raise ValueError(
                'dirCoef must be defined in the directional case.')
        _validate_float('dirCoef', dirCoef)

    b_N = np.zeros((kr.size, N + 1), dtype='complex')

    for n in range(N + 1):

        if arrayType is 'open':
            b_N[:, n] = 4 * np.pi * np.power(1j, n) * spherical_jn(n, kr)

        elif arrayType is 'rigid':
            jn = spherical_jn(n, kr)
            jnprime = spherical_jn(n, kr, derivative=True)
            hn = sph_hankel2(n, kr)
            hnprime = dsph_hankel2(n, kr)

            temp = 4 * np.pi * np.power(1j, n) * (jn -
                                                  (jnprime / hnprime) * hn)
            temp[np.where(kr == 0)] = 4 * np.pi if n == 0 else 0.
            b_N[:, n] = temp

        elif arrayType is 'directional':
            jn = spherical_jn(n, kr)
            jnprime = spherical_jn(n, kr, derivative=True)

            temp = 4 * np.pi * np.power(1j, n) * (dirCoef * jn - 1j *
                                                  (1 - dirCoef) * jnprime)
            b_N[:, n] = temp

    return b_N
示例#15
0
def sph_array_noise(R, nMic, maxN, arrayType, f):
    """
    Returns noise amplification curves of a spherical mic. array

    Parameters
    ----------
    R : float
        Microphone array radius, in meter.
    nMic : int
        Number of microphone capsules.
    maxN : int
        Maximum spherical harmonic expansion order.
    arrayType : str
        'open' or 'rigid'.
    f : ndarray
        Frequencies where to perform estimation. Dimension = (l).

    Returns
    -------
    g2 : ndarray
        Noise amplification curve. Dimension = (l, maxN)
    g2_lin: ndarray
        Linear log-log interpolation of noise. Dimension = (l, maxN).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    The `arrayType` options are:
    - 'open' for open array of omnidirectional sensors, or
    - 'rigid' for sensors mounted on a rigid baffle.

    `g2_lin` is an approximation of the curves at low frequencies showing
    the linear behaviour in the log-log axis, with a 6n dB slope.

    """

    _validate_float('R', R, positive=True)
    _validate_int('nMic', nMic, positive=True)
    _validate_int('maxN', maxN, positive=True)
    _validate_string('arrayType', arrayType, choices=['open', 'rigid'])
    _validate_ndarray_1D('f', f, positive=True)

    # Frequency axis
    kR = 2 * np.pi * f * R / c
    # Modal responses
    bN = sph_modal_coefs(maxN, kR, arrayType) / (4 * np.pi)
    # Noise power response
    g2 = 1. / (nMic * np.power(np.abs(bN), 2))

    # Approximate linearly
    p = -(6 / 10) * np.arange(1, maxN + 1) / np.log10(2)
    bN_lim0 = (sph_modal_coefs(maxN, np.asarray([1]), arrayType) /
               (4 * np.pi)).squeeze()
    a = 1. / (nMic * np.power(np.abs(bN_lim0[1:]), 2))

    g2_lin = np.zeros((kR.size, maxN))
    for n in range(maxN):
        g2_lin[:, n] = a[n] * np.power(kR, p[n])

    return g2, g2_lin
示例#16
0
def filter_rirs(rir, f_center, fs):
    """
    Apply a filterbank to a given impulse responses.

    Parameters
    ----------
    rir : ndarray
        Impulse responses to be filtered.  Dimension = (L, nBands)
    f_center : ndarray
        Center frequencies of the filterbank. Dimension = (nBands)
    fs : int
        Target sampling rate

    Returns
    -------
    ir : ndarray
        Filtered impulse responses. Dimension = (L+M, 1)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    Filter operation is implemented with `scipy.signal.firwin`.
    Order of the filters is hardcoded to M = 1000 (length=M+1).

    The highest center frequency must be at most equal to fs/2, in order to avoid aliasing.
    The lowest center frequency must be at least equal to 30 Hz.
    Center frequencies must increase monotonically.

    TODO: expose filter order, minimum frequency as parameter?
    """

    nBands = rir.shape[1]
    _validate_ndarray_2D('rir', rir)
    _validate_int('fs', fs, positive=True)
    _validate_ndarray_1D('f_center',
                         f_center,
                         positive=True,
                         size=nBands,
                         limit=[30, fs / 2])

    if nBands == 1:
        rir_full = rir
    else:
        order = 1000
        filters = np.zeros((order + 1, nBands))
        for i in range(nBands):
            if i == 0:
                fl = 30.
                fh = np.sqrt(f_center[i] * f_center[i + 1])
                wl = fl / (fs / 2.)
                wh = fh / (fs / 2.)
                filters[:, i] = scipy.signal.firwin(order + 1, [wl, wh],
                                                    pass_zero='bandpass')
            elif i == nBands - 1:
                fl = np.sqrt(f_center[i] * f_center[i - 1])
                w = fl / (fs / 2.)
                filters[:, i] = scipy.signal.firwin(order + 1,
                                                    w,
                                                    pass_zero='highpass')
            else:
                fl = np.sqrt(f_center[i] * f_center[i - 1])
                fh = np.sqrt(f_center[i] * f_center[i + 1])
                wl = fl / (fs / 2.)
                wh = fh / (fs / 2.)
                filters[:, i] = scipy.signal.firwin(order + 1, [wl, wh],
                                                    pass_zero='bandpass')

        temp_rir = np.append(rir, np.zeros((order, nBands)), axis=0)
        rir_filt = scipy.signal.fftconvolve(filters, temp_rir,
                                            axes=0)[:temp_rir.shape[0], :]
        rir_full = np.sum(rir_filt, axis=1)[:, np.newaxis]

    return rir_full
示例#17
0
def render_rirs_array(echograms, band_centerfreqs, fs, grids, array_irs):
    """
    Render the echogram IRs of an array of mic arrays with arbitrary geometries and transfer functions.

    Parameters
    ----------
    echograms : ndarray, dtype = Echogram
        Target echograms. Dimension = (nSrc, nRec, nBands)
    band_centerfreqs : ndarray
        Center frequencies of the filterbank. Dimension = (nBands)
    fs : int
        Target sampling rate
    grids : List
        DoA grid for each receiver. Length = (nRec)
    array_irs : List
        IR of each element of the eceivers. Length = (nRec)

    Returns
    -------
    rirs : List
        RIR for each receiver element. Length = (nRec)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    For each microphone array (receiver position), we must provide two parameters:
    `grids` contains the angular positions, in azimuth-elevation pairs (in radians),
        of the sampled measured/computed DoAs of the array. ndarray with dimension = (nDoa, C-1).
    `array_irs` is the time-domain IR from each DoA measurement point to each microphone capsule.
        It is therefore a ndarray with dimension = (L1, nMic, nDoa).
    These parameters are independent for each receiver, but `nDoa` must macth within receiver.

    Each of the elements in the algorithm output list `rirs` is a ndarray with dimension = (L2, nMic, nSrc),
    and contains the Room Impulse Response for each capsule/source pair at each receiver (microphone array).

    The highest center frequency must be at most equal to fs/2, in order to avoid aliasing.
    The lowest center frequency must be at least equal to 30 Hz.
    Center frequencies must increase monotonically.

    TODO: expose fractional, L_filterbank as parameter?
    """

    nSrc = echograms.shape[0]
    nRec = echograms.shape[1]
    nBands = echograms.shape[2]

    _validate_echogram_array(echograms)
    _validate_int('fs', fs, positive=True)
    _validate_ndarray_1D('f_center',
                         band_centerfreqs,
                         positive=True,
                         size=nBands,
                         limit=[30, fs / 2])
    _validate_list('grids', grids, size=nRec)
    for i in range(nRec):
        _validate_ndarray_2D('grids_' + str(i), grids[i], shape1=C - 1)
    _validate_list('array_irs', array_irs, size=nRec)
    for i in range(nRec):
        _validate_ndarray_3D('array_irs_' + str(i),
                             array_irs[i],
                             shape2=grids[i].shape[0])

    # Sample echogram to a specific sampling rate with fractional interpolation
    fractional = True

    # Decide on number of samples for all RIRs
    endtime = 0
    for ns in range(nSrc):
        for nr in range(nRec):
            temptime = echograms[ns, nr, 0].time[-1]
            if temptime > endtime:
                endtime = temptime

    L_rir = int(np.ceil(endtime * fs))
    L_fbank = 1000 if nBands > 1 else 0

    array_rirs = [None] * nRec
    for nr in range(nRec):
        grid_dirs_rad = grids[nr]
        nGrid = np.shape(grid_dirs_rad)[0]
        mic_irs = array_irs[nr]
        L_resp = np.shape(mic_irs)[0]
        nMics = np.shape(mic_irs)[1]
        array_rirs[nr] = np.zeros((L_rir + L_fbank + L_resp - 1, nMics, nSrc))

        for ns in range(nSrc):
            print('Rendering echogram: Source ' + str(ns) + ' - Receiver ' +
                  str(nr))
            print('      Quantize echograms to receiver grid')
            echo2gridMap = get_echo2gridMap(echograms[ns, nr, 0],
                                            grid_dirs_rad)

            tempRIR = np.zeros((L_rir, nGrid, nBands))
            for nb in range(nBands):

                # First step: reflections are quantized to the grid directions
                q_echograms = quantise_echogram(echograms[ns, nr, nb], nGrid,
                                                echo2gridMap)
                # Second step: render quantized echograms
                print('      Rendering quantized echograms: Band ' + str(nb))
                tempRIR[:, :,
                        nb], _ = render_quantised(q_echograms, endtime, fs,
                                                  fractional)

            tempRIR2 = np.zeros((L_rir + L_fbank, nGrid))
            print('      Filtering and combining bands')
            for ng in range(nGrid):
                tempRIR2[:, ng] = filter_rirs(tempRIR[:, ng, :],
                                              band_centerfreqs, fs).squeeze()

            # Third step: convolve with directional IRs at grid directions
            idx_nonzero = [
                i for i in range(tempRIR2.shape[1])
                if np.sum(np.power(tempRIR2[:, i], 2)) > 10e-12
            ]  # neglect grid directions with almost no energy
            tempRIR2 = np.row_stack((tempRIR2[:, idx_nonzero],
                                     np.zeros((L_resp - 1, len(idx_nonzero)))))
            for nm in range(nMics):
                tempResp = mic_irs[:, nm, idx_nonzero]
                array_rirs[nr][:, nm, ns] = np.sum(scipy.signal.fftconvolve(
                    tempResp, tempRIR2, axes=0)[:tempRIR2.shape[0], :],
                                                   axis=1)

    return array_rirs
示例#18
0
def render_rirs_sh(echograms, band_centerfreqs, fs):
    """
    Render a spherical harmonic echogram array into an impulse response matrix.

    Parameters
    ----------
    echograms : ndarray, dtype = Echogram
        Target echograms. Dimension = (nSrc, nRec, nBands)
    band_centerfreqs : ndarray
        Center frequencies of the filterbank. Dimension = (nBands)
    fs : int
        Target sampling rate

    Returns
    -------
    ir : ndarray
        Rendered echograms. Dimension = (M, maxSH, nRec, nSrc)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `maxSH` is the highest spherical harmonic number found in all echograms.
    For any echogram with nSH<maxSH, the channels (nSH...maxSH) will contain only zeros.

    The highest center frequency must be at most equal to fs/2, in order to avoid aliasing.
    The lowest center frequency must be at least equal to 30 Hz.
    Center frequencies must increase monotonically.

    TODO: expose fractional, L_filterbank as parameter?
    """

    # echograms: [nSrc, nRec, nBands] dimension
    nSrc = echograms.shape[0]
    nRec = echograms.shape[1]
    nBands = echograms.shape[2]
    _validate_echogram_array(echograms)
    _validate_int('fs', fs, positive=True)
    _validate_ndarray_1D('f_center',
                         band_centerfreqs,
                         positive=True,
                         size=nBands,
                         limit=[30, fs / 2])

    # Sample echogram to a specific sampling rate with fractional interpolation
    fractional = True

    # Decide on number of samples for all RIRs
    endtime = 0
    for ns in range(nSrc):
        for nr in range(nRec):
            temptime = echograms[ns, nr, 0].time[-1]
            if temptime > endtime:
                endtime = temptime

    L_rir = int(np.ceil(endtime * fs))
    L_fbank = 1000 if nBands > 1 else 0
    L_tot = L_rir + L_fbank

    # Find maximum number of SH channels in all echograms
    maxSH = 0
    for nr in range(nRec):
        tempSH = np.shape(echograms[0, nr, 0].value)[1]
        if tempSH > maxSH:
            maxSH = tempSH

    # Render responses and apply filterbank to combine different decays at different bands
    rirs = np.empty((L_tot, maxSH, nRec, nSrc))
    for ns in range(nSrc):
        for nr in range(nRec):

            print('Rendering echogram: Source ' + str(ns) + ' - Receiver ' +
                  str(nr))
            nSH = np.shape(echograms[ns, nr, 0].value)[1]

            tempIR = np.zeros((L_rir, nSH, nBands))
            for nb in range(nBands):
                tempIR[:, :, nb] = render_rirs(echograms[ns, nr, nb], endtime,
                                               fs, fractional)

            print('     Filtering and combining bands')
            for nh in range(nSH):
                rirs[:, nh, nr,
                     ns] = filter_rirs(tempIR[:, nh, :], band_centerfreqs,
                                       fs).squeeze()
    return rirs
示例#19
0
def sph_array_alias_lim(R, nMic, maxN, mic_dirs_rad, mic_weights=None):
    """
    Get estimates of the aliasing limit of a spherical array, in three different ways.

    Parameters
    ----------
    R : float
        Microphone array radius, in meter.
    nMic : int
        Number of microphone capsules.
    maxN : int
        Maximum spherical harmonic expansion order.
    mic_dirs_rad : ndarray
        Evaluation directions. Dimension = (nDirs, 2).
        Directions are expected in radians, expressed in pairs [azimuth, elevation].
    dirCoef: ndarray, optional
       Vector of weights used to improve orthogonality of the SH transform. Dimension = (nDirs)

    Returns
    -------
    f_alias : ndarray
       the alising limit estimates. Dimension = (3).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    First estimate takes into account only the radius and a nominal order
    that the array is expected to support, it is the simplest one and it
    expresses the kR = maxN rule.
    The second estimate is based on the number of microphones, and it can
    be more relaxed than the first, if the nominal supported order is less
    than maxN<floor(sqrt(Nmic)-1).
    The third takes into account microphone numbers and directions, and it
    is based on the orthogonality of the SH matrix for the microphone
    positions expressed through the condition number.

    # todo check RETURN VALUES (need for very big rtol if cond_N returned)
    """

    _validate_float('R', R, positive=True)
    _validate_int('nMic', nMic, positive=True)
    _validate_int('maxN', maxN, positive=True)
    _validate_ndarray_2D('mic_dirs_rad', mic_dirs_rad, shape1=C - 1)
    if mic_weights is not None:
        _validate_ndarray_1D('mic_weights',
                             mic_weights,
                             size=mic_dirs_rad.shape[0])

    f_alias = np.zeros(3)

    # Conventional kR = N assumption
    f_alias[0] = c * maxN / (2 * np.pi * R)

    # Based on the floor of the number of microphones, uniform arrangement
    f_alias[1] = c * np.floor(np.sqrt(nMic) - 1) / (2 * np.pi * R)

    # Based on condition number of the SHT matrix
    maxOrder = int(np.ceil(np.sqrt(nMic) - 1))

    cond_N = check_cond_number_sht(maxOrder, elev2incl(mic_dirs_rad), 'real',
                                   mic_weights)
    trueMaxOrder = np.flatnonzero(
        cond_N < np.power(10, 4))[-1]  # biggest element passing the condition
    f_alias[2] = c * trueMaxOrder / (2 * np.pi * R)

    return f_alias
示例#20
0
def simulate_cyl_array(N_filt, mic_dirs_rad, src_dirs_rad, arrayType, R,
                       N_order, fs):
    """
    Simulate the impulse responses of a cylindrical array.

    Parameters
    ----------
    N_filt : int
        Number of frequencies where to compute the response. It must be even.
    mic_dirs_rad: ndarray
        Directions of microphone capsules, in radians. Dimension = (N_mic).
    src_dirs_rad: ndarray
        Direction of arrival of the indicent plane waves, in radians. Dimension = (N_doa).
    arrayType: str
        'open' or 'rigid'.
        Target sampling rate
    R: float
        Radius of the array cylinder, in meter.
    N_order: int
        Maximum cylindrical harmonic expansion order.
    fs: int
        Sample rate.

    Returns
    -------
    h_mic: ndarray
        Computed IRs in time-domain. Dimension = (N_filt, N_mic, N_doa).
    H_mic: ndarray, dtype='complex'
        Frequency responses of the computed IRs. Dimension = (N_filt//2+1, N_mic, N_doa).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    This method computes the impulse responses of the microphones of a
    cylindrical microphone array for the given directions of incident plane waves.
    The array type can be either 'open' for omnidirectional microphones in
    an open setup, or 'rigid' for omnidirectional microphones mounted on a cylinder.

    """

    _validate_int('N_filt', N_filt, positive=True, parity='even')
    _validate_ndarray_1D('mic_dirs_rad', mic_dirs_rad)
    _validate_ndarray_1D('src_dirs_rad', src_dirs_rad)
    _validate_string('arrayType', arrayType, choices=['open', 'rigid'])
    _validate_float('R', R, positive=True)
    _validate_int('N_order', N_order, positive=True)
    _validate_int('fs', fs, positive=True)

    # Compute the frequency-dependent part of the microphone responses (radial dependence)
    f = np.arange(N_filt // 2 + 1) * fs / N_filt
    kR = 2 * np.pi * f * R / masp.c
    b_N = asr.cyl_modal_coefs(N_order, kR, arrayType)

    # Handle Nyquist for real impulse response
    temp = b_N.copy()
    temp[-1, :] = np.real(temp[-1, :])
    # Create the symmetric conjugate negative frequency response for a real time-domain signal
    b_Nt = np.real(
        np.fft.fftshift(np.fft.ifft(np.append(temp,
                                              np.conj(temp[-2:0:-1, :]),
                                              axis=0),
                                    axis=0),
                        axes=0))

    # Compute angular-dependent part of the microphone responses
    # Unit vectors of DOAs and microphones
    N_doa = src_dirs_rad.shape[0]
    N_mic = mic_dirs_rad.shape[0]
    h_mic = np.zeros((N_filt, N_mic, N_doa))
    H_mic = np.zeros((N_filt // 2 + 1, N_mic, N_doa), dtype='complex')

    for i in range(N_doa):
        angle = mic_dirs_rad - src_dirs_rad[i]
        C = np.zeros((N_order + 1, N_mic))
        for n in range(N_order + 1):
            # Jacobi-Anger expansion
            if n == 0:
                C[n, :] = np.ones(angle.shape)
            else:
                C[n, :] = 2 * np.cos(n * angle)
        h_mic[:, :, i] = np.matmul(b_Nt, C)
        H_mic[:, :, i] = np.matmul(b_N, C)

    return h_mic, H_mic
示例#21
0
def compute_echograms_mic(room, src, rec, abs_wall, limits, mic_specs):
    """
    Compute the echogram response of individual microphones for a given acoustic scenario.

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    src : ndarray
        Source position in cartesian coordinates. Dimension = (nSrc, 3) [[x, y, z]].
    rec : ndarray
        Receiver position in cartesian coordinates. Dimension = (nRec, 3) [[x, y, z]].
    abs_wall : ndarray
        Wall absorption coefficients per band. Dimension = (nBands, 6)
    limits : ndarray
        Maximum echogram computation time per band.  Dimension = (nBands)
    mic_specs : ndarray
        Microphone directions and directivity factor. Dimension = (nRec, 4)

    Returns
    -------
    abs_echograms : ndarray, dtype = Echogram
        Array with rendered echograms. Dimension = (nSrc, nRec, nBands)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `src` and `rec` positions are specified from the left ground corner
    of the room, using a left-handed coordinate system.
    `room` refers to the wall dimensions.
    Therefore, their values should be positive and smaller than room dimensions.

              _____    _
             |     |   |
             |     |   |
           x ^     |   | l = r[0]
             |     |   |
             |     |   |
             o---->    -
                  y
             |-----|
                w = r[1]

    `abs_wall` must have all values in the range [0,1].
    `nBands` will be determined by the length of `abs_wall` first dimension.

    Each row of `mic_specs` is expected to be described as [x, y, z, alpha],
    with (x, y, z) begin the unit vector of the mic orientation.
    `alpha` must be contained in the range [0(dipole), 1(omni)],
    so that directivity is expressed as: d(theta) = a + (1-a)*cos(theta).


    TODO: expose type as parameter?, validate return
    """

    nRec = rec.shape[0]
    nSrc = src.shape[0]
    nBands = abs_wall.shape[0]

    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_2D('src', src, shape1=C, positive=True)
    _validate_ndarray_2D('rec', rec, shape1=C, positive=True)
    _validate_ndarray_2D('abs_wall', abs_wall, shape1=2*C, positive=True)
    _validate_ndarray_1D('limits', limits, positive=True, size=nBands)
    _validate_ndarray_2D('mic_specs', mic_specs, shape0=nRec, shape1=C+1)

    # Limit the RIR by reflection order or by time-limit
    type = 'maxTime'

    # Compute echogram due to pure propagation (frequency-independent)
    echograms = np.empty((nSrc, nRec), dtype=Echogram)
    for ns in range(nSrc):
        for nr in range(nRec):
            print('Compute echogram: Source ' + str(ns) + ' - Receiver ' + str(nr))
            # Compute echogram
            echograms[ns, nr] = ims_coreMtx(room, src[ns,:], rec[nr,:], type, np.max(limits))

    print('Apply receiver direcitivites')
    rec_echograms = rec_module_mic(echograms, mic_specs)

    abs_echograms = np.empty((nSrc, nRec, nBands), dtype=Echogram)
    # Apply boundary absorption
    for ns in range(nSrc):
        for nr in range(nRec):
            print('Apply absorption: Source ' + str(ns) + ' - Receiver ' + str(nr))
            # Compute echogram
            abs_echograms[ns, nr] = apply_absorption(rec_echograms[ns, nr], abs_wall, limits)

    # return abs_echograms, rec_echograms, echograms
    return abs_echograms
示例#22
0
def cylindrical_scatterer(mic_dirs_rad, src_dirs_rad, R, N_order, N_filt, fs):
    """
    Compute the pressure due to a cylindrical scatterer

    The function computes the impulse responses of the pressure measured
    at some points in the field with a cylindrical rigid scatterer centered
    at the origin and due to incident plane waves.

    Parameters
    ----------
    mic_dirs_rad: ndarray
        Position of microphone capsules. Dimension = (N_mic, C-1).
        Positions are expected in radians, expressed in pairs [azimuth, distance].
    src_dirs_rad: ndarray
        Direction of arrival of the indicent plane waves. Dimension = (N_doa).
        Directions (azimuths) are expected in radians.
    R: float
        Radius of the array sphere, in meter.
    N_order: int
        Maximum cylindrical harmonic expansion order.
    N_filt : int
        Number of frequencies where to compute the response. It must be even.
    fs: int
        Sample rate.

    Returns
    -------
    h_mic: ndarray
        Computed IRs in time-domain. Dimension = (N_filt, Nmic, Ndoa)
    H_mic: ndarray, dtype='complex'
        Frequency responses of the computed IRs. Dimension = (N_filt//2+1, N_mic, N_doa).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    """

    _validate_ndarray_2D('mic_dirs_rad', mic_dirs_rad, shape1=masp.C - 1)
    _validate_ndarray_1D('src_dirs_rad', src_dirs_rad)
    _validate_float('R', R, positive=True)
    _validate_int('N_order', N_order, positive=True)
    _validate_int('N_filt', N_filt, positive=True, parity='even')
    _validate_int('fs', fs, positive=True)
    if np.any(mic_dirs_rad[:, 1] < R):
        raise ValueError(
            'mic_dirs_rad: The distance of the measurement point cannot be less than the radius:'
            + str(R))

    K = N_filt // 2 + 1
    f = np.arange(K) * fs / N_filt
    kR = 2 * np.pi * f * R / masp.c
    N_mic = mic_dirs_rad.shape[0]
    N_doa = src_dirs_rad.size

    # Check if all microphones at same radius
    same_radius = np.sum(mic_dirs_rad[1:, 1] - mic_dirs_rad[:-1, 1]) == 0
    if same_radius:
        # Cylindrical modal coefs for rigid sphere
        b_N = np.zeros((K, N_order + 1), dtype='complex')
        r = mic_dirs_rad[0, 1]
        kr = 2 * np.pi * f * r / masp.c

        # Similar to the cyl_modal_coefs for the rigid case
        for n in range(N_order + 1):
            jn = jv(n, kr)
            jnprime = jvp(n, kR, 1)
            hn = hankel2(n, kr)
            hnprime = h2vp(n, kR, 1)
            b_N[:, n] = np.power(1j, n) * (jn - (jnprime / hnprime) * hn)

    else:
        # Cylindrical modal coefs for rigid cylinder, but at different distances
        b_N = np.zeros((K, N_order + 1, N_mic), dtype='complex')
        for nm in range(N_mic):
            r = mic_dirs_rad[nm, 1]
            kr = 2 * np.pi * f * r / masp.c

            # Similar to the sph_modal_coefs for the rigid case
            for n in range(N_order + 1):
                jn = jv(n, kr)
                jnprime = jvp(n, kR, 1)
                hn = hankel2(n, kr)
                hnprime = h2vp(n, kR, 1)
                b_N[:, n,
                    nm] = np.power(1j, n) * (jn - (jnprime / hnprime) * hn)

    # Avoid NaNs for very high orders, instead of (very) very small values
    b_N[np.isnan(b_N)] = 0.

    # Compute angular-dependent part of the microphone responses
    H_mic = np.zeros((K, N_mic, N_doa), dtype='complex')
    for nd in range(N_doa):
        # Unit vectors of DOAs and microphones
        azi0 = src_dirs_rad[nd]
        azi = mic_dirs_rad[:, 0]
        angle = azi - azi0

        C = np.zeros((N_order + 1, N_mic))
        for n in range(N_order + 1):
            # Jacobi-Anger expansion
            if n == 0:
                C[n, :] = np.ones(angle.shape)
            else:
                C[n, :] = 2 * np.cos(n * angle)
        # Accumulate across orders
        if same_radius:
            H_mic[:, :, nd] = np.matmul(b_N, C)
        else:
            for nm in range(N_mic):
                H_mic[:, nm, nd] = np.matmul(b_N[:, :, nm], C[:, nm])

    # Handle Nyquist for real impulse response
    tempH_mic = H_mic.copy()
    # TODO: in `simulate_sph_array()` it was real, not abs. Why?
    tempH_mic[-1, :] = np.abs(tempH_mic[-1, :])
    # Conjugate ifft and fftshift for causal IR
    h_mic = np.real(
        np.fft.fftshift(np.fft.ifft(np.append(tempH_mic,
                                              np.conj(tempH_mic[-2:0:-1, :]),
                                              axis=0),
                                    axis=0),
                        axes=0))

    return h_mic, H_mic
示例#23
0
def room_stats(room, abs_wall, verbose=True):
    """
    Estimate RT60 through Sabine's method.

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    abs_wall : ndarray
        Wall absorption coefficients per band. Dimension = (nBands, 6)
    verbose: bool, optional
        Display room stats. Default to False.

    Returns
    -------
    rt60 : float
        Estimated reverberation time.
    d_critical: float
        Estimated critical distance.
    d_mfpath: float
        Estimated mean free path.

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    As opposed to `find_abs_coeffs_from_rt()`, `abs_wall_ratios` must be explicit.

    `alpha` and `abs_wall_ratios` must have all values in the range [0,1].

    """

    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_2D('abs_wall', abs_wall, shape1=2*C, norm=True)

    l, w, h = room
    V = l*w*h   # room volume
    Stot = 2 * ( (l * w) + (l * h) + (w * h) ) # room area

    # Analyse in frequency bands
    nBands = abs_wall.shape[0]
    a_mean = np.empty(nBands)
    rt60_sabine = np.empty(nBands)
    for m in range(nBands):
        a_x = abs_wall[m,0:2]
        a_y = abs_wall[m,2:4]
        a_z = abs_wall[m,4:6]
        # mean absorption
        a_mean[m] = sum( (w * h * a_x) + (l * h * a_y) + (l * w * a_z)) / Stot
        rt60_sabine[m] = (55.25 * V) / (c * Stot * a_mean[m])

    d_critical = 0.1 * np.sqrt(V / (np.pi * rt60_sabine))
    d_mfpath = 4 * V / Stot

    if verbose:
        print('Room dimensions (m)          ' + str(l) + 'x' + str(w) + 'x' + str(h))
        print('Room volume (m^3)            ' + str(V))
        print('Mean absorption coeff        ' + str(a_mean))
        print('Sabine Rev. Time 60dB (sec)  ' + str(rt60_sabine))
        print('Critical distance (m)        ' + str(d_critical))
        print('Mean free path (m)           ' + str(d_mfpath))

    return rt60_sabine, d_critical, d_mfpath
示例#24
0
def find_abs_coeffs_from_rt(room, rt60_target, abs_wall_ratios=None):
    """
    Compute wall absorption coefficients per frequency band and wall.

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    rt60_target : ndarray
        Target reverberation time. Dimension = (nBands).
    abs_wall_ratios : ndarray, optional
        Wall absorption coefficient ratios. Dimension = (6).

    Returns
    -------
    alpha_walls : ndarray
        Wall absorption coefficients . Dimension = (nBands, 6).
    rt60_true : ndarray
        RT60 time computed from result. Dimension = (nBands).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    nBands will be determined by the length of rt60_target.

    If `abs_wall_ratios` is not specified, no wall absorption is applied.

    abs_wall_ratios are expected to be normalized to 1.
    The method will automatically normalize them, in case.

    """

    # Validate arguments
    _validate_ndarray_1D('room', room, size=3, positive=True)
    _validate_ndarray_1D('rt60_target', rt60_target, positive=True)
    if abs_wall_ratios is not None:
        _validate_ndarray_1D('abs_wall_ratios', abs_wall_ratios, size=6, positive=True)


    # Default wall absorption
    if abs_wall_ratios is None:
        abs_wall_ratios = np.ones(6)

    # Normalize
    abs_wall_ratios = abs_wall_ratios / np.max(abs_wall_ratios)

    nBands = len(rt60_target)
    rt60_true = np.zeros(nBands)
    alpha_walls = np.zeros((nBands,6))

    for nb in range(nBands):
        rt60 = rt60_target[nb]
        fmin = lambda alpha: np.abs(rt60 - get_rt_sabine(alpha, room, abs_wall_ratios))
        alpha = scipy.optimize.fmin(func=fmin, x0=0.0001, disp=False)
        rt60_true[nb] = rt60 + fmin(alpha)
        alpha_walls[nb,:] = alpha * abs_wall_ratios

    return alpha_walls, rt60_true
示例#25
0
def evaluate_sht_filters(M_mic2sh,
                         H_array,
                         fs,
                         Y_grid,
                         w_grid=None,
                         plot=False):
    """
    Evaluate frequency-dependent performance of SHT filters.

    Parameters
    ----------
    M_mic2sh : ndarray
        SHT filtering matrix produced by one of the methods included in the library.
        Dimension = ( (order+1)^2, nMics, nBins ).
    H_array : ndarray, dtype = 'complex'
         Modeled or measured spherical array responses in a dense grid of `nGrid` directions.
        Dimension = ( nBins, nMics, nGrid ).
    fs : int
        Target sampling rate.
    Y_grid : ndarray
        Spherical harmonics matrix for the `nGrid` directions of the evaluation grid.
        Dimension = ( nGrid, (order+1)^2 ).
    w_grid : ndarray, optional
        Vector of integration weights for the grid points.
        Dimension = ( nGrid ).
    plot : bool, optional
        Plot responses. Default to false.

    Returns
    -------
    cSH : ndarray, dtype = 'complex'
        Spatial correlation coefficient, for each SHT order and frequency bin.
        Dimension = ( nBins, order+1 ).
    lSH : ndarray
        Level difference, for each SHT order, for each SHT order and frequency bin.
        Dimension = ( nBins, order+1 ).
    WNG : ndarray
        Maximum amplification of all output SH components.
        Dimension = ( nBins ).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    The SHT filters can be evaluated in terms of how ideal are the SH
    components that they generate. The evaluation here follows the metrics
    introduced in

        Moreau, S., Daniel, J., Bertet, S., 2006,
        `3D sound field recording with higher order ambisonics-objectiv
        measurements and validation of spherical microphone.`
        In Audio Engineering Society Convention 120.

    These are a) the spatial correlation coefficient between each ideal
    spherical harmonic and the reconstructed pattern, evaluated at a dense
    grid of directions, b) level difference between the mean spatial power
    of the reconstructed pattern (diffuse power) over the one from an ideal
    SH component. Ideally, correlaiton should be close to one, and the
    level difference should be close to 0dB.

    Additionally, the maximum amplification of all output SH components is
    evaluated, through the maximum eigenvalue of the filtering matrix.

    Due to the matrix nature of computations,
    the minimum valid value for `nMics` and `nGrid` is 2.
    """

    _validate_ndarray_3D('M_mic2sh', M_mic2sh)
    n_sh = M_mic2sh.shape[0]
    order_sht = int(np.sqrt(n_sh) - 1)
    nMics = M_mic2sh.shape[1]
    _validate_number('nMics', nMics, limit=[2, np.inf])
    nBins = M_mic2sh.shape[2]

    _validate_ndarray_3D('H_array', H_array, shape0=nBins, shape1=nMics)
    nGrid = H_array.shape[2]
    _validate_number('nGrid', nGrid, limit=[2, np.inf])

    _validate_ndarray_2D('Y_grid', Y_grid, shape0=nGrid, shape1=n_sh)

    if w_grid is None:
        w_grid = 1 / nGrid * np.ones(nGrid)
    _validate_ndarray_1D('w_grid', w_grid, size=nGrid)

    _validate_int('fs', fs, positive=True)
    if plot is not None:
        _validate_boolean('plot', plot)

    nFFT = 2 * (nBins - 1)
    f = np.arange(nFFT // 2 + 1) * fs / nFFT

    # Compute spatial correlations and integrated level difference between
    # ideal and reconstructed harmonics
    cSH = np.empty((nBins, order_sht + 1), dtype='complex')
    lSH = np.empty((nBins, order_sht + 1))
    # rSH = np.empty((nBins, order_sht+1))
    for kk in range(nBins):
        H_kk = H_array[kk, :, :]
        y_recon_kk = np.matmul(M_mic2sh[:, :, kk], H_kk)
        for n in range(order_sht + 1):
            cSH_n = 0  # spatial correlation (mean per order)
            lSH_n = 0  # diffuse level difference (mean per order)
            # rSH_n = 0  # mean level difference (mean per order)
            for m in range(-n, n + 1):
                q = np.power(n, 2) + n + m
                y_recon_nm = y_recon_kk[q, :].T
                y_ideal_nm = Y_grid[:, q]
                cSH_nm = np.matmul(
                    (y_recon_nm * w_grid).conj(), y_ideal_nm) / np.sqrt(
                        np.matmul((y_recon_nm * w_grid).conj(), y_recon_nm))
                cSH_n = cSH_n + cSH_nm
                lSH_nm = np.real(
                    np.matmul((y_recon_nm * w_grid).conj(), y_recon_nm))
                lSH_n = lSH_n + lSH_nm
                # rSH_nm = np.sum(np.power(np.abs(y_recon_nm - y_ideal_nm), 2) * w_grid)
                # rSH_n = rSH_n + rSH_nm;
            cSH[kk, n] = cSH_n / (2 * n + 1)
            lSH[kk, n] = lSH_n / (2 * n + 1)
            # rSH[kk, n] = rSH_n / (2 * n + 1)

    # Maximum noise amplification of all filters in matrix
    WNG = np.empty(nBins)
    for kk in range(nBins):
        # TODO: Matlab implementation warns when M matrix is complex, e.g. TEST_SCRIPTS l. 191-199
        # Avoid ComplexWarning: imaginary parts appearing due to numerical precission
        eigM = np.real(
            np.linalg.eigvals(
                np.matmul(M_mic2sh[:, :, kk].T.conj(), M_mic2sh[:, :, kk])))
        WNG[kk] = np.max(eigM)

    # Plots
    if plot:
        str_legend = [None] * (order_sht + 1)
        for n in range(order_sht + 1):
            str_legend[n] = str(n)

        plt.figure()
        plt.subplot(311)
        plt.semilogx(f, np.abs(cSH))
        plt.grid()
        plt.legend(str_legend)
        plt.axis([50, 20000, 0, 1])
        plt.title('Spatial correlation')

        plt.subplot(312)
        plt.semilogx(f, 10 * np.log10(lSH))
        plt.grid()
        plt.legend(str_legend)
        plt.axis([50, 20000, -30, 10])
        plt.title('Level correlation')

        plt.subplot(313)
        plt.semilogx(f, 10 * np.log10(WNG))
        plt.grid()
        plt.xlim([50, 20000])
        plt.title('Maximum amplification')
        plt.xlabel('Frequency (Hz)')

        # plt.subplot(414)
        # plt.semilogx(f, 10 * np.log10(rSH))
        # plt.grid()
        # plt.xlim([50, 20000])
        # plt.title('MSE')
        # plt.xlabel('Frequency (Hz)')

        plt.show()

    return cSH, lSH, WNG
示例#26
0
def array_sht_filters_measure_regLSHD(H_array,
                                      order_sht,
                                      grid_dirs_rad,
                                      w_grid=None,
                                      nFFT=1024,
                                      amp_threshold=10.):
    """
    Generate SHT filters based on measured responses  (regularized least-squares in the SHD)

    Parameters
    ----------
    H_array : ndarray, dtype=complex
        Frequency domain measured array responses. Dimension = ( nFFT//2+1, nMics, nGrids )
    order_sht : int
        Spherical harmonic transform order.
    grid_dirs_rad: ndarray,
        Grid positions in [azimuth, elevation] pairs (in radians). Dimension = (nGrid, 2).
    w_grid : ndarray, optional
        Weights for weighted-least square solution, based on the importance or area
        around each measurement point (leave empty if not known, or not important)
        Dimension = ( nGrid ).
    nFFT : int, optional
        Number of points for the FFT.
    amp_threshold : float
        Max allowed amplification for filters, in dB.

    Returns
    -------
    h_filt : ndarray
        Impulse responses of the filters. Dimension = ( (order_sht+1)^2, nMics, nFFT ).
    H_filt: ndarray
        Frequency-domain filters. Dimension = ( (order_sht+1)^2, nMics, nFFT//2+1 ).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.
    UserWarning: if `nMic` not big enough for the required sht order.
    TODO: ValueError: if the array order is not big enough.

    Notes
    -----
    Generate the filters to convert micorphone signals from a spherical
    microphone array to SH signals, based on a least-squares solution with
    a constraint on noise amplification, using Tikhonov regularization. The
    method formulates the LS problem in the space domain, using the
    directional measurements of the array response, similar to e.g.

        Moreau, S., Daniel, J., Bertet, S., 2006,
        3D sound field recording with higher order ambisonics-objective
        measurements and validation of spherical microphone.
        In Audio Engineering Society Convention 120.

    # TODO: nFFT argument is redundant!!!
    Due to the matrix nature of computations,
    the minimum valid value for `nMics` and `nGrid` is 2 and 16 respectively.
    """

    _validate_int('nFFT', nFFT, positive=True, parity='even')
    nBins = nFFT // 2 + 1

    _validate_ndarray_3D('H_array', H_array, shape0=nBins)
    nMic = H_array.shape[1]
    _validate_number('nMic', nMic, limit=[2, np.inf])
    nGrid = H_array.shape[2]
    _validate_number('nGrid', nGrid, limit=[16, np.inf])

    _validate_int('order_sht', order_sht, positive=True)
    _validate_ndarray_2D('grid_dirs_rad', grid_dirs_rad, shape1=C - 1)

    if w_grid is None:
        w_grid = 1 / nGrid * np.ones(nGrid)
    _validate_ndarray_1D('w_grid', w_grid, size=nGrid)

    _validate_float('amp_threshold', amp_threshold)

    # Adequate sht order to the number of microphones
    if order_sht > np.sqrt(nMic) - 1:
        order_sht = int(np.floor(np.sqrt(nMic) - 1))
        warnings.warn(
            "Set order too high for the number of microphones, should be N<=np.sqrt(Q)-1. Auto set to "
            + str(order_sht), UserWarning)

    order_array = int(np.floor(np.sqrt(nGrid) / 2 - 1))
    # TODO: check validity of the approach
    # order_array must be greater or equal than requested order_sht ( nGrid > (2*(order_sht+1))^2 )
    if order_array < order_sht:
        raise ValueError("Order array < Order SHT. Consider increasing nGrid")

    # SH matrix at grid directions
    Y_grid = np.sqrt(
        4 * np.pi) * get_sh(order_array, elev2incl(grid_dirs_rad),
                            'real').T  # SH matrix for grid directions

    # Compute inverse matrix
    a_dB = amp_threshold
    alpha = complex(np.power(
        10, a_dB / 20))  # Explicit casting to allow negative sqrt (a_dB < 0)
    beta = 1 / (2 * alpha)
    W_grid = np.diag(w_grid)
    H_nm = np.zeros((nBins, nMic, np.power(order_array + 1, 2)),
                    dtype='complex')
    for kk in range(nBins):
        tempH = H_array[kk, :, :]
        H_nm[kk, :, :] = np.matmul(
            np.matmul(np.matmul(tempH, W_grid), Y_grid.T),
            np.linalg.inv(np.matmul(np.matmul(Y_grid, W_grid), Y_grid.T)))

    # Compute the inverse matrix in the SHD with regularization
    H_filt = np.zeros((np.power(order_sht + 1, 2), nMic, nBins),
                      dtype='complex')
    for kk in range(nBins):
        tempH_N = H_nm[kk, :, :]
        tempH_N_trunc = tempH_N[:, :np.power(order_sht + 1, 2)]
        H_filt[:, :, kk] = np.matmul(
            tempH_N_trunc.T.conj(),
            np.linalg.inv(
                np.matmul(tempH_N, tempH_N.T.conj()) +
                np.power(beta, 2) * np.eye(nMic)))

    # Time domain filters
    h_filt = H_filt.copy()
    h_filt[:, :, -1] = np.abs(h_filt[:, :, -1])
    h_filt = np.concatenate((h_filt, np.conj(h_filt[:, :, -2:0:-1])), axis=2)
    h_filt = np.real(np.fft.ifft(h_filt, axis=2))
    h_filt = np.fft.fftshift(h_filt, axes=2)

    # TODO: check return ordering
    return h_filt, H_filt
示例#27
0
def compute_echograms_array(room, src, rec, abs_wall, limits):
    """
    Compute the echogram response of a microphone array for a given acoustic scenario.

    Parameters
    ----------
    room : ndarray
        Room dimensions in cartesian coordinates. Dimension = (3) [x, y, z].
    src : ndarray
        Source position in cartesian coordinates. Dimension = (nSrc, 3) [[x, y, z]].
    rec : ndarray
        Receiver position in cartesian coordinates. Dimension = (nRec, 3) [[x, y, z]].
    abs_wall : ndarray
        Wall absorption coefficients per band. Dimension = (nBands, 6)
    limits : ndarray
        Maximum echogram computation time per band.  Dimension = (nBands)

    Returns
    -------
    abs_echograms : ndarray, dtype = Echogram
        Array with rendered echograms. Dimension = (nSrc, nRec, nBands)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `src` and `rec` positions are specified from the left ground corner
    of the room, using a left-handed coordinate system.
    `room` refers to the wall dimensions.
    Therefore, their values should be positive and smaller than room dimensions.

              _____    _
             |     |   |
             |     |   |
           x ^     |   | l = r[0]
             |     |   |
             |     |   |
             o---->    -
                  y
             |-----|
                w = r[1]

    `abs_wall` must have all values in the range [0,1].
    `nBands` will be determined by the length of `abs_wall` first dimension.

    TODO: expose type as parameter?, validate return
    """

    nRec = rec.shape[0]
    nSrc = src.shape[0]
    nBands = abs_wall.shape[0]

    _validate_ndarray_1D('room', room, size=C, positive=True)
    _validate_ndarray_2D('src', src, shape1=C, positive=True)
    _validate_ndarray_2D('rec', rec, shape1=C, positive=True)
    _validate_ndarray_2D('abs_wall', abs_wall, shape1=2*C, positive=True)
    _validate_ndarray_1D('limits', limits, positive=True, size=nBands)

    # Limit the RIR by reflection order or by time-limit
    type = 'maxTime'

    echograms = np.empty((nSrc, nRec), dtype=Echogram)
    # Compute echogram due to pure propagation (frequency-independent)
    for ns in range(nSrc):
        for nr in range(nRec):
            print('Compute echogram: Source ' + str(ns) + ' - Receiver ' + str(nr))
            # Compute echogram
            echograms[ns, nr] = ims_coreMtx(room, src[ns,:], rec[nr,:], type, np.max(limits))

    abs_echograms = np.empty((nSrc, nRec, nBands), dtype=Echogram)
    # Apply boundary absorption
    for ns in range(nSrc):
        for nr in range(nRec):
            print('Apply absorption: Source ' + str(ns) + ' - Receiver ' + str(nr))
            # Compute echogram
            abs_echograms[ns, nr] = apply_absorption(echograms[ns, nr], abs_wall, limits)

    # return abs_echograms, echograms
    return abs_echograms
def test_validate_ndarray_1D():

    # TypeError: not a ndarray
    wrong_values = ['1', True, 3j, [2.3], None, np.nan, np.inf]
    for wv in wrong_values:
        with pytest.raises(TypeError, match='must be an instance of ndarray'):
            _validate_ndarray_1D('vw', wv)

    # ValueError: not 1D
    wrong_values = [np.empty(()), np.empty((1, 2)), np.empty((1, 2, 3))]
    for wv in wrong_values:
        with pytest.raises(ValueError, match='must be 1D'):
            _validate_ndarray_1D('vw', wv)

    # ValueError: not given size
    size = 3
    wrong_values = [np.empty((1)), np.empty((2)), np.empty((4))]
    for wv in wrong_values:
        with pytest.raises(ValueError, match='must have size'):
            _validate_ndarray_1D('vw', wv, size=size)

    # ValueError: not norm
    wrong_values = [
        np.ones((3)) * 2,
        np.ones((3)) * -1,
        np.asarray([0., 1., 2.])
    ]
    for wv in wrong_values:
        with pytest.raises(ValueError, match='must be in the interval'):
            _validate_ndarray_1D('vw', wv, norm=True)

    # ValueError: not positive
    wrong_values = [np.ones((3)) * -1, np.asarray([0., -1., 2.])]
    for wv in wrong_values:
        with pytest.raises(ValueError, match='must be positive'):
            _validate_ndarray_1D('vw', wv, positive=True)

    # ValueError: outside limits
    limit = [-1.5, 0.5]
    wrong_values = [np.ones((3)), np.asarray([0., -1., -2.])]
    for wv in wrong_values:
        with pytest.raises(ValueError):
            _validate_ndarray_1D('vw', wv, limit=limit)

    # ValueError: wrong dtype
    dtype = np.dtype(float)
    wrong_values = [
        np.ones((3), dtype='int'),
        np.asarray([0., -1., 2.], dtype='O')
    ]
    for wv in wrong_values:
        with pytest.raises(TypeError, match='dtype must be'):
            _validate_ndarray_1D('vw', wv, dtype=dtype)
示例#29
0
def get_array_response(src_dirs,
                       mic_pos,
                       N_filt,
                       fs=48000,
                       mic_dirs=None,
                       fDir_handle=None):
    """
    Return array response of directional sensors.

    The function computes the impulse responses of the microphones of
    an open array of directional microphones, located at R_mic and with
    orientations U_orient, for the directions-of-incidence U_doa. Each
    sensors directivity in defined by a function handle in the cell array
    fDir_handle.

    Parameters
    ----------
    src_dirs: ndarray
        Direction of arrival of the indicent plane waves, in cartesian coordinates.
        Dimension = (Ndoa, C).
    mic_pos: ndarray
        Position of microphone capsules, in cartesian coordinates.
        Dimension = (Nmic, C).
    N_filt: int
        Number of frequencies where to compute the responses. It must be even.
    fs: int, optional.
        Sample rate. Default to 48000 Hz.
    mic_dirs: optional.
        Orientation of microphone capsules, in cartesian coordinates.
        Default to None. See notes.
    fDir_handle: optional.
        Microphone directivity functions.
        Default to None. See notes.

    Returns
    -------
    h_mic: ndarray
        Computed IRs in time-domain. Dimension = (N_filt, Nmic, Ndoa)
    H_mic: ndarray, dtype='complex'
        Frequency responses of the computed IRs. Dimension = (N_filt//2+1, N_mic, N_doa).

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    In the general case, `U_orient` defines the orientation (in cartesian) of each
    microphone capsule. Therefore, it is expected to be a 2D ndarray with dimension (Nmic, C).
    It is possible to define the same orientation for all capsules. In this case,
    `U_orient` is expected to be a 1D array of length C.
    If `U_orient` is left unespecified, the orientation of the microphones is assumed
    to be radial from the origin.

    `fDir_hadle` allows the specification of custom capsulse directivity polar patterns,
    expressed as lambda expressions with one parameter (lambda angle: f(angle)).
    In the general case, `fDir_hadle` is expected to be a 1D ndarray of length `Nmic`,
    with each lambda corresponding to the directivity of a microphone capsule.
    It is possible to define the directivity for all capsules. In this case,
    `fDir_hadle` is expected to be a lambda expression.
    If `fDir_hadle` is left unespecified, the function assumes omnidirectional direcivities
    for all capsules.


    Examples
    -----
    Simulate the response of a 3-microphone array of an omnidirectional microphone,
    a first-order cardioid and a second-order cardioid, with random locations and
    orientations, for front and side incidence:

    Nmic = 3
    N_filt = 1000
    U_doa = np.asarray([[1, 0, 0],[0, 1, 0]])
    R_mic = np.random.rand(Nmic,3)
    U_orient = np.random.rand(Nmic,3)
    U_orient/np.tile(np.sqrt(np.sum(np.power(U_orient,2),axis=1)), (3,1)).T # Unit vector

    fdir_omni = lambda angle: np.ones(np.size(angle))
    fdir_card = lambda angle: (1/2)*(1 + np.cos(angle));
    fdir_card2 = lambda angle: np.power(1/2,2) * np.power((1 + np.cos(angle)),2)
    fDir_handle = np.asarray([fdir_omni, fdir_card, fdir_card2])

    h_mic, H_mic = ars.get_array_response(U_doa, R_mic, U_orient=U_orient,
                                            fDir_handle=fDir_handle, N_filt=N_filt)

    import matplotlib.pyplot as plt
    plt.plot(h_mic[:,:,0])
    plt.show()
    """

    Ndoa = src_dirs.shape[0]
    Nmics = mic_pos.shape[0]
    _validate_ndarray_2D('src_dirs', src_dirs, shape1=masp.C)
    _validate_ndarray_2D('mic_pos', mic_pos, shape1=masp.C)
    _validate_int('N_filt', N_filt, positive=True, parity='even')
    _validate_int('fs', fs, positive=True)

    # If no directivity coefficient is defined assume omnidirectional sensors
    if fDir_handle is None:
        # Expand to vector of omni lambdas
        fDir_handle = np.asarray([lambda angle: 1 for i in range(Nmics)])
    else:
        if masp.isLambda(fDir_handle):
            fDir_handle = np.asarray([fDir_handle for i in range(Nmics)])
        else:
            _validate_ndarray_1D('fDir_handle', fDir_handle, size=Nmics)
            for i in range(Nmics):
                assert masp.isLambda(fDir_handle[i])

    # Compute unit vectors of the microphone positionsT
    normR_mic = np.sqrt(np.sum(np.power(mic_pos, 2), axis=1))
    U_mic = mic_pos / normR_mic[:, np.newaxis]

    # If no orientation is defined then assume that the microphones
    # are oriented radially, similar to U_mic
    if mic_dirs is None:
        mic_dirs = U_mic
    else:
        _validate_ndarray('mic_dirs', mic_dirs)
        if mic_dirs.ndim == 1:
            _validate_ndarray_1D('mic_dirs', mic_dirs, size=masp.C)
            mic_dirs = np.tile(mic_dirs, (Nmics, 1))
        else:
            _validate_ndarray_2D('mic_dirs', mic_dirs, shape1=masp.C)

    # Frequency vector
    Nfft = N_filt
    K = Nfft // 2 + 1
    f = np.arange(K) * fs / Nfft

    # Unit vectors pointing to the evaluation points
    U_eval = np.empty((Ndoa, Nmics, masp.C))
    U_eval[:, :, 0] = np.tile(src_dirs[:, 0], (Nmics, 1)).T
    U_eval[:, :, 1] = np.tile(src_dirs[:, 1], (Nmics, 1)).T
    U_eval[:, :, 2] = np.tile(src_dirs[:, 2], (Nmics, 1)).T

    # Computation of time delays and attenuation for each evaluation point to microphone,
    # measured from the origin
    tempR_mic = np.empty((Ndoa, Nmics, masp.C))
    tempR_mic[:, :, 0] = np.tile(mic_pos[:, 0], (Ndoa, 1))
    tempR_mic[:, :, 1] = np.tile(mic_pos[:, 1], (Ndoa, 1))
    tempR_mic[:, :, 2] = np.tile(mic_pos[:, 2], (Ndoa, 1))

    tempU_orient = np.empty((Ndoa, Nmics, masp.C))
    tempU_orient[:, :, 0] = np.tile(mic_dirs[:, 0], (Ndoa, 1))
    tempU_orient[:, :, 1] = np.tile(mic_dirs[:, 1], (Ndoa, 1))
    tempU_orient[:, :, 2] = np.tile(mic_dirs[:, 2], (Ndoa, 1))

    # cos-angles between DOAs and sensor orientations
    cosAngleU = np.sum(U_eval * tempU_orient, axis=2)
    # d*cos-angles between DOAs and sensor positions
    dcosAngleU = np.sum(U_eval * tempR_mic, axis=2)

    # Attenuation due to directionality of the sensors
    B = np.zeros((Ndoa, Nmics))
    for nm in range(Nmics):
        B[:, nm] = fDir_handle[nm](np.arccos(cosAngleU[:, nm]))

    # Create TFs for each microphone
    H_mic = np.zeros((K, Nmics, Ndoa), dtype='complex')
    for kk in range(K):
        omega = 2 * np.pi * f[kk]
        tempTF = B * np.exp(1j * (omega / masp.c) * dcosAngleU)
        H_mic[kk, :, :] = tempTF.T

    # Create IRs for each microphone
    h_mic = np.zeros((Nfft, Nmics, Ndoa))
    for nd in range(Ndoa):
        tempTF = H_mic[:, :, nd].copy()
        tempTF[-1, :] = np.abs(tempTF[-1, :])
        tempTF = np.append(tempTF, np.conj(tempTF[-2:0:-1, :]), axis=0)
        h_mic[:, :, nd] = np.real(np.fft.ifft(tempTF, axis=0))
        h_mic[:, :, nd] = np.fft.fftshift(h_mic[:, :, nd], axes=0)

    return h_mic, H_mic
示例#30
0
def apply_absorption(echogram, alpha, limits=None):
    """
    Applies per-band wall absorption to a given echogram.

    Parameters
    ----------
    echogram : Echogram
        Target Echogram
    alpha : ndarray
        Wall absorption coefficients per band. Dimension = (nBands, 6)
    limits : ndarray, optional
        Maximum reflection time per band (RT60). Dimension = (nBands)

    Returns
    -------
    abs_echograms : ndarray, dtype = Echogram
        Array with echograms subject to absorption. Dimension = (1, nBands)

    Raises
    -----
    TypeError, ValueError: if method arguments mismatch in type, dimension or value.

    Notes
    -----
    `nBands` will be determined by the length of `alpha` first dimension.

    `alpha` must have all values in the range [0,1].

    If 'limits' is not specified, no wall absorption is applied.

    """

    # Validate arguments
    _validate_echogram(echogram)
    _validate_ndarray_2D('abs_wall', alpha, shape1=2 * C, norm=True)
    nBands = alpha.shape[0]
    if limits is not None:
        _validate_ndarray_1D('limits', limits, size=nBands, positive=True)

    abs_echograms = np.empty(nBands, dtype=Echogram)

    if limits is None:
        for i in range(nBands):
            abs_echograms[i] = copy.copy(echogram)
    else:
        for nb in range(nBands):
            # Find index of last echogram time element smaller than the given limit
            idx_limit = np.arange(len(
                echogram.time))[echogram.time < limits[nb]][-1]
            # idx_limit = echogram.time[echogram.time < limits[nb]].size
            abs_echograms[nb] = Echogram(value=echogram.value[:idx_limit + 1],
                                         time=echogram.time[:idx_limit + 1],
                                         order=echogram.order[:idx_limit + 1],
                                         coords=echogram.coords[:idx_limit +
                                                                1])

    for nb in range(nBands):

        # Absorption coefficients for x, y, z walls per frequency
        a_x = alpha[nb, 0:2]
        a_y = alpha[nb, 2:4]
        a_z = alpha[nb, 4:6]
        # Reflection coefficients
        r_x = np.sqrt(1 - a_x)
        r_y = np.sqrt(1 - a_y)
        r_z = np.sqrt(1 - a_z)

        # Split
        i = abs_echograms[nb].order[:, 0]
        j = abs_echograms[nb].order[:, 1]
        k = abs_echograms[nb].order[:, 2]

        i_even = i[np.remainder(i, 2) == 0]
        i_odd = i[np.remainder(i, 2) != 0]
        i_odd_pos = i_odd[i_odd > 0]
        i_odd_neg = i_odd[i_odd < 0]

        j_even = j[np.remainder(j, 2) == 0]
        j_odd = j[np.remainder(j, 2) != 0]
        j_odd_pos = j_odd[j_odd > 0]
        j_odd_neg = j_odd[j_odd < 0]

        k_even = k[np.remainder(k, 2) == 0]
        k_odd = k[np.remainder(k, 2) != 0]
        k_odd_pos = k_odd[k_odd > 0]
        k_odd_neg = k_odd[k_odd < 0]

        # Find total absorption coefficients by calculating the
        # number of hits on every surface, based on the order per dimension
        abs_x = np.zeros(np.size(abs_echograms[nb].time))
        abs_x[np.remainder(i, 2) == 0] = np.power(
            r_x[0], (np.abs(i_even) / 2.)) * np.power(r_x[1],
                                                      (np.abs(i_even) / 2.))
        abs_x[(np.remainder(i, 2) != 0)
              & (i > 0)] = np.power(r_x[0], np.ceil(
                  i_odd_pos / 2.)) * np.power(r_x[1], np.floor(i_odd_pos / 2.))
        abs_x[(np.remainder(i, 2) != 0) & (i < 0)] = np.power(
            r_x[0], np.floor(np.abs(i_odd_neg) / 2.)) * np.power(
                r_x[1], np.ceil(np.abs(i_odd_neg) / 2.))

        abs_y = np.zeros(np.size(abs_echograms[nb].time))
        abs_y[np.remainder(j, 2) == 0] = np.power(
            r_y[0], (np.abs(j_even) / 2.)) * np.power(r_y[1],
                                                      (np.abs(j_even) / 2.))
        abs_y[(np.remainder(j, 2) != 0)
              & (j > 0)] = np.power(r_y[0], np.ceil(
                  j_odd_pos / 2.)) * np.power(r_y[1], np.floor(j_odd_pos / 2.))
        abs_y[(np.remainder(j, 2) != 0) & (j < 0)] = np.power(
            r_y[0], np.floor(np.abs(j_odd_neg) / 2.)) * np.power(
                r_y[1], np.ceil(np.abs(j_odd_neg) / 2.))

        abs_z = np.zeros(np.size(abs_echograms[nb].time))
        abs_z[np.remainder(k, 2) == 0] = np.power(
            r_z[0], (np.abs(k_even) / 2.)) * np.power(r_z[1],
                                                      (np.abs(k_even) / 2.))
        abs_z[(np.remainder(k, 2) != 0) & (k > 0)] = np.power(
            r_z[0], np.ceil(k_odd_pos / 2.)) * np.power(
                r_z[1], np.floor(k_odd_pos / 2, ))
        abs_z[(np.remainder(k, 2) != 0) & (k < 0)] = np.power(
            r_z[0], np.floor(np.abs(k_odd_neg) / 2.)) * np.power(
                r_z[1], np.ceil(np.abs(k_odd_neg) / 2.))

        s_abs_tot = abs_x * abs_y * abs_z
        # Final amplitude of reflection
        abs_echograms[nb].value = (
            s_abs_tot * abs_echograms[nb].value.transpose()).transpose()

    return abs_echograms[np.newaxis, :]