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
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)
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
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
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
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
def array_sht_filters_theory_regLS(R, mic_dirsAziElev, order_sht, Lfilt, fs, amp_threshold): """ Generate SHT filters based on theoretical responses (regularized least-squares) Parameters ---------- R : float Microphone array radius, in meter. mic_dirsAziElev : ndarray Evaluation directions. Dimension = (nDirs, 2). Directions are expected in radians, expressed in pairs [azimuth, elevation]. order_sht : int Spherical harmonic transform order. Lfilt : int Number of FFT points for the output filters. It must be even. fs : int Sample rate for the output filters. amp_threshold : float Max allowed amplification for filters, in dB. Returns ------- h_filt : ndarray Impulse responses of the filters. Dimension = ( nMic, (order_sht+1)^2, Lfilt ). H_filt: ndarray Frequency-domain filters. Dimension = ( nMic, (order_sht+1)^2, Lfilt//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 microphone signals from a spherical microphone array to SH signals, based on an ideal theoretical model of the array. The filters are generated as a least-squares solution with a constraint on filter amplification, using Tikhonov regularization. The method formulates the LS problem in the spherical harmonic domain, by expressing the array response to an order-limited series of SH coefficients, similar to Jin, C.T., Epain, N. and Parthy, A., 2014. Design, optimization and evaluation of a dual-radius spherical microphone array. IEEE/ACM Transactions on Audio, Speech, and Language Processing, 22(1), pp.193-204. """ _validate_float('R', R, positive=True) _validate_ndarray_2D('mic_dirsAziElev', mic_dirsAziElev, shape1=C - 1) _validate_int('order_sht', order_sht, positive=True) _validate_int('Lfilt', Lfilt, positive=True, parity='even') _validate_int('fs', fs, positive=True) _validate_float('amp_threshold', amp_threshold) f = np.arange(Lfilt // 2 + 1) * fs / Lfilt num_f = f.size kR = 2 * np.pi * f * R / c kR_max = kR[-1] nMic = mic_dirsAziElev.shape[0] # 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) mic_dirsAziIncl = elev2incl(mic_dirsAziElev) order_array = int(np.floor(2 * kR_max)) # TODO: check validity of the approach if order_array <= 1: raise ValueError("Order array <= 1. Consider increasing R or fs.") Y_array = np.sqrt(4 * np.pi) * get_sh(order_array, mic_dirsAziIncl, 'real') # Modal responses bN = sph_modal_coefs(order_array, kR, 'rigid') / ( 4 * np.pi ) # due to modified SHs, the 4pi term disappears from the plane wave expansion # Array response in the SHD H_array = np.zeros((nMic, np.power(order_array + 1, 2), num_f), dtype='complex') for kk in range(num_f): temp_b = bN[kk, :].T B = np.diag(replicate_per_order(temp_b)) H_array[:, :, kk] = np.matmul(Y_array, B) 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) H_filt = np.zeros((np.power(order_sht + 1, 2), nMic, num_f), dtype='complex') for kk in range(num_f): tempH_N = H_array[:, :, 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
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
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
def render_quantised(qechogram, endtime, fs, fractional): """ Render a quantised echogram array into a quantised impulse response matrix. Parameters ---------- qechograms : ndarray, dtype = QuantisedEchogram Target quantised echograms. Dimension = (nDirs). endtime : float Maximum time of rendered reflections, in seconds. fs : int Target sampling rate fractional : bool, optional Use fractional or integer (round) delay. Default to True. Returns ------- qIR : ndarray Rendered quantised echograms. Dimension = (ceil(endtime * fs), nChannels) idx_nonzero : 1D ndarray Indices of non-zero elements. Raises ----- TypeError, ValueError: if method arguments mismatch in type, dimension or value. TODO: expose fractional as parameter? """ _validate_quantised_echogram_array(qechogram) _validate_float('edntime', endtime, positive=True) _validate_int('fs', fs, positive=True) _validate_boolean('fractional', fractional) # Number of grid directions of the quantization nDirs = qechogram.size # Render echograms L_rir = int(np.ceil(endtime * fs)) qIR = np.zeros((L_rir, nDirs)) idx_nonzero = [] for nq in range(nDirs): tempgram = Echogram( time=qechogram[nq].time, value=qechogram[nq].value, order=np.zeros((qechogram[nq].time.size, 3), dtype=int), # whatever to pass the size validation coords=np.zeros((qechogram[nq].time.size, 3))) # whatever to pass the size validation # Omit if there are no echoes in the specific one if qechogram[nq].isActive: idx_nonzero.append(nq) # Number of reflections inside the time limit') idx_limit = tempgram.time[tempgram.time < endtime].size tempgram.time = tempgram.time[:idx_limit + 1] tempgram.value = tempgram.value[:idx_limit + 1] tempgram.order = tempgram.order[:idx_limit + 1] # whatever to pass the size validation tempgram.coords = tempgram.coords[:idx_limit + 1] # whatever to pass the size validation qIR[:, nq] = render_rirs(tempgram, endtime, fs, fractional).squeeze() return qIR, np.asarray(idx_nonzero)
def render_rirs(echogram, endtime, fs, fractional=True): """ Render an echogram into an impulse response. Parameters ---------- echogram : Echogram Target Echogram. endtime : float Maximum time of rendered reflections, in seconds. fs : int Target sampling rate fractional : bool, optional Use fractional or integer (round) delay. Default to True. Returns ------- ir : ndarray Rendered echogram. Dimension = (ceil(endtime * fs), nChannels) Raises ----- TypeError, ValueError: if method arguments mismatch in type, dimension or value. Notes ----- TODO: expose filter order as parameter? """ _validate_echogram(echogram) _validate_float('endtime', endtime, positive=True) _validate_int('fs', fs, positive=True) _validate_boolean('fractional', fractional) nChannels = 1 if np.ndim(echogram.value) <= 1 else np.shape( echogram.value)[1] L_ir = int(np.ceil(endtime * fs)) ir = np.zeros((L_ir, nChannels)) # Number of reflections inside the time limit idx_trans = echogram.time[echogram.time < endtime].size if fractional: # Get lagrange interpolating filter of order 100 (filter length 101) order = 100 h_offset = 50 h_idx = np.arange(-(order / 2), (order / 2) + 1).astype( int) # only valid for even orders # Make a filter table for quick access for quantized fractional samples fractions = np.linspace(0, 1, 101) H_frac = lagrange(order, 50 + fractions) # Initialise array tmp_ir = np.zeros((int(L_ir + (2 * h_offset)), nChannels)) for i in range(idx_trans): refl_idx = int(np.floor(echogram.time[i] * fs) + 1) refl_frac = np.remainder(echogram.time[i] * fs, 1) filter_idx = np.argmin(np.abs(refl_frac - fractions)) h_frac = H_frac[:, filter_idx] tmp_ir[h_offset + refl_idx + h_idx - 1, :] += h_frac[:, np.newaxis] * echogram.value[i] ir = tmp_ir[h_offset:-h_offset, :] else: refl_idx = (np.round(echogram.time[:idx_trans] * fs)).astype(int) # Filter out exceeding indices refl_idx = refl_idx[refl_idx < L_ir] ir[refl_idx, :] = echogram.value[:refl_idx.size] return ir
def sph_array_noise_threshold(R, nMic, maxG_db, maxN, arrayType, dirCoef=None): """ Returns frequency limits for noise amplification of a spherical mic. array Parameters ---------- R : float Microphone array radius, in meter. nMic : int Number of microphone capsules. maxG_db : float max allowed amplification for the noise level. maxG_db = 20*log10(maxG) maxN : int Maximum spherical harmonic expansion order. arrayType: str 'open', 'rigid' or 'directional' dirCoef: float, optional Directivity coefficient of the sensor. Default to None. Returns ------- f_lim : ndarray Frequency points where threhsold is reached, for orders 1:maxN. Dimension = (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, - '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). The method returns the frequencies that the noise in the output channels of a SMA, after performing the SHT and equalization of the output signals, reaches a certain user-defined threshold maxG_db. The frequencies are computed only at the lower range of each order, where its response decays rapidly, ignoring for example the nulls of an open array at the higher frequencies. The estimation of the limits are based on a linear approximation of the log-log response found e.g. in Sector-based Parametric Sound Field Reproduction in the Spherical Harmonic Domain A Politis, J Vilkamo, V Pulkki IEEE Journal of Selected Topics in Signal Processing 9 (5), 852 - 866 """ _validate_float('R', R, positive=True) _validate_int('nMic', nMic, positive=True) _validate_float('maxG_db', maxG_db) _validate_int('maxN', maxN, 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) f_lim = np.zeros(maxN) for n in range(1, maxN + 1): bn = sph_modal_coefs(n, np.asarray([1]), arrayType, dirCoef) / (4 * np.pi) bn = bn.flatten()[-1] maxG = np.power(10, (maxG_db / 10)) kR_lim = np.power((maxG * nMic * np.power(np.abs(bn), 2)), (-10 * np.log10(2) / (6 * n))) f_lim[n - 1] = kR_lim * c / (2 * np.pi * R) return f_lim
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
def compute_echograms_sh(room, src, rec, abs_wall, limits, sh_orders): """ Compute the echogram response of individual microphones for a given acoustic scenario, in the spherical harmonic domain. 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) sh_orders : int or ndarray, dtype = int Spherical harmonic expansion order. Dimension = 1 or (nRec) 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. If `sh_orders` is an integer, the given order will be applied to all receivers. 'nRec' will be determined by the length of `rec` 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) if isinstance(sh_orders, int): _validate_int('sh_orders', sh_orders, positive=True) sh_orders = sh_orders * np.ones(nRec, dtype=int) else: _validate_ndarray_1D('sh_orders', sh_orders, size=nRec, positive=True, dtype=int) # 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 SH directivites') rec_echograms = rec_module_sh(echograms, sh_orders) 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
def spherical_scatterer(mic_dirs_rad, src_dirs_rad, R, N_order, N_filt, fs): """ Compute the pressure due to a spherical scatterer The function computes the impulse responses of the pressure measured at some points in the field with a spherical 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). Positions are expected in radians, expressed in triplets [azimuth, elevation, distance]. src_dirs_rad: ndarray Direction of arrival of the indicent plane waves. Dimension = (N_doa, C-1). Directions are expected in radians, expressed in pairs [azimuth, elevation]. 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) _validate_ndarray_2D('src_dirs_rad', src_dirs_rad, shape1=masp.C - 1) _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[:, 2] < R): raise ValueError( 'mic_dirs_rad: The distance of the measurement point cannot be less than the radius:' + str(R)) # Compute the frequency-dependent part of the microphone responses (radial dependence) 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.shape[0] # Check if all microphones at same radius same_radius = np.sum(mic_dirs_rad[1:, 2] - mic_dirs_rad[:-1, 2]) == 0 if same_radius: # Spherical modal coefs for rigid sphere b_N = np.zeros((K, N_order + 1), dtype='complex') r = mic_dirs_rad[0, 2] 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 = spherical_jn(n, kr) jnprime = spherical_jn(n, kR, derivative=True) hn = asr.sph_hankel2(n, kr) hnprime = asr.dsph_hankel2(n, kR) b_N[:, n] = (2 * n + 1) * np.power(1j, n) * (jn - (jnprime / hnprime) * hn) else: # Spherical modal coefs for rigid sphere, 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, 2] 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 = spherical_jn(n, kr) jnprime = spherical_jn(n, kR, derivative=True) hn = asr.sph_hankel2(n, kr) hnprime = asr.dsph_hankel2(n, kR) b_N[:, n, nm] = (2 * n + 1) * 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, 0] elev0 = src_dirs_rad[nd, 1] azi = mic_dirs_rad[:, 0] elev = mic_dirs_rad[:, 1] cosAlpha = np.sin(elev) * np.sin(elev0) + np.cos(elev) * np.cos( elev0) * np.cos(azi - azi0) P_N = np.zeros((N_order + 1, N_mic)) for n in range(N_order + 1): for nm in range(N_mic): P_N[n, nm] = lpmn(n, n, cosAlpha[nm])[0][0, -1] # Accumulate across orders if same_radius: H_mic[:, :, nd] = np.matmul(b_N, P_N) else: for nm in range(N_mic): H_mic[:, nm, nd] = np.matmul(b_N[:, :, nm], P_N[:, 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
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
def array_sht_filters_theory_softLim(R, nMic, order_sht, Lfilt, fs, amp_threshold): """ Generate SHT filters based on theoretical responses (soft-limiting) Parameters ---------- R : float Microphone array radius, in meter. nMic : int Number of microphone capsules. order_sht : int Spherical harmonic transform order. Lfilt : int Number of FFT points for the output filters. It must be even. fs : int Sample rate for the output filters. amp_threshold : float Max allowed amplification for filters, in dB. Returns ------- h_filt : ndarray Impulse responses of the filters. Dimension = (Lfilt, order_sht+1). H_filt: ndarray Frequency-domain filters. Dimension = (Lfilt//2+1, order_sht+1). Raises ----- TypeError, ValueError: if method arguments mismatch in type, dimension or value. UserWarning: if `nMic` not big enough for the required sht order. Notes ----- Generate the filters to convert microphone signals from a spherical microphone array to SH signals, based on an ideal theoretical model of the array. The filters are generated from an inversion of the radial components of the response, neglecting spatial aliasing effects and non-ideal arrangements of the microphones. One filter is shared by all SH signals of the same order in this case. Here this single channel inversion problem is done through a thresholding approach on the inversion, limited to a max allowed amplification. The limiting follows the approach of Bernschutz, B., Porschmann, C., Spors, S., Weinzierl, S., Versterkung, B., 2011. Soft-limiting der modalen amplitudenverst?rkung bei sph?rischen mikrofonarrays im plane wave decomposition verfahren. Proceedings of the 37. Deutsche Jahrestagung fur Akustik (DAGA 2011) """ _validate_float('R', R, positive=True) _validate_int('nMic', nMic, positive=True) _validate_int('order_sht', order_sht, positive=True) _validate_int('Lfilt', Lfilt, positive=True, parity='even') _validate_int('fs', fs, positive=True) _validate_float('amp_threshold', amp_threshold) f = np.arange(Lfilt // 2 + 1) * fs / Lfilt # 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) # Modal responses kR = 2 * np.pi * f * R / c bN = sph_modal_coefs(order_sht, kR, 'rigid') / ( 4 * np.pi ) # due to modified SHs, the 4pi term disappears from the plane wave expansion # Single channel equalization filters per order inv_bN = 1. / bN inv_bN[0, 1:] = 0. # Encoding matrix with regularization a_dB = amp_threshold alpha = complex(np.sqrt(nMic) * np.power( 10, a_dB / 20)) # Explicit casting to allow negative sqrt (a_dB < 0) # Regularized single channel equalization filters per order H_filt = (2 * alpha / np.pi) * (np.abs(bN) * inv_bN) * np.arctan( (np.pi / (2 * alpha)) * np.abs(inv_bN)) # Time domain filters temp = H_filt.copy() temp[-1, :] = np.real(temp[-1, :]) # Create the symmetric conjugate negative frequency response for a real time-domain signal h_filt = np.real( np.fft.fftshift(np.fft.ifft(np.append(temp, np.conj(temp[-2:0:-1, :]), axis=0), axis=0), axes=0)) # TODO: check return order return h_filt, H_filt
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
def get_sh(N, dirs, basisType): """ Get spherical harmonics up to order N evaluated at given angular positions. Parameters ---------- N : int Maximum spherical harmonic expansion order. 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'. Returns ------- Y : ndarray Spherical harmonics . Dimension = (nDirs, nHarm). Raises ----- TypeError, ValueError: if method arguments mismatch in type, dimension or value. NotImplementedError: for basisType = 'complex' Notes ----- Ouput dimension is given by: nHarm = (N+1)^2 Inclination is defined as the angle from zenith: inclination = pi/2-elevation TODO: implement complex basis? """ _validate_int('order', N, positive=True) _validate_ndarray_2D('dirs', dirs, shape1=C - 1) _validate_string('basisType', basisType, choices=['complex', 'real']) nDirs = dirs.shape[0] nHarm = np.power(N + 1, 2) Y_N = np.zeros((nDirs, nHarm)) def delta_kronecker(q1, q2): return 1 if q1 == q2 else 0 # TODO # it looks like the output of shs is N3d (max 1, sqrt(3)!3) # so it needs to be scaled as * np.sqrt(4*np.pi) * [1, 1./np.sqrt(3), 1./np.sqrt(3), 1./np.sqrt(3)] def norm(m): """ TODO SN3D FACTOR, REMOVE CONDON SHOTLEY IT MUST BE MULTIPLIED BY sqrt(4PI) to be normalized to 1 :param m: :return: """ return np.power(-1, np.abs(m)) * np.sqrt(2 - delta_kronecker(0, np.abs(m))) if basisType is 'complex': # TODO raise NotImplementedError elif basisType is 'real': harm_idx = 0 for l in range(N + 1): for m in range(-l, 0): Y_N[:, harm_idx] = np.imag( sph_harm(np.abs(m), l, dirs[:, 0], dirs[:, 1])) * norm(m) harm_idx += 1 for m in range(l + 1): Y_N[:, harm_idx] = np.real( sph_harm(m, l, dirs[:, 0], dirs[:, 1])) * norm(m) harm_idx += 1 return Y_N
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
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
def array_sht_filters_theory_radInverse(R, nMic, order_sht, Lfilt, fs, amp_threshold): """ Generate SHT filters based on theoretical responses (regularized radial inversion) Parameters ---------- R : float Microphone array radius, in meter. nMic : int Number of microphone capsules. order_sht : int Spherical harmonic transform order. Lfilt : int Number of FFT points for the output filters. It must be even. fs : int Sample rate for the output filters. amp_threshold : float Max allowed amplification for filters, in dB. Returns ------- h_filt : ndarray Impulse responses of the filters. Dimension = (Lfilt, order_sht+1). H_filt: ndarray Frequency-domain filters. Dimension = (Lfilt//2+1, order_sht+1). Raises ----- TypeError, ValueError: if method arguments mismatch in type, dimension or value. UserWarning: if `nMic` not big enough for the required sht order. Notes ----- Generate the filters to convert microphone signals from a spherical microphone array to SH signals, based on an ideal theoretical model of the array. The filters are generated from an inversion of the radial components of the response, neglecting spatial aliasing effects and non-ideal arrangements of the microphones. One filter is shared by all SH signals of the same order in this case. Here this single channel inversion problem is done with a constraint on max allowed amplification regularization, using Tikhonov regularization, similar e.g. to: 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. """ _validate_float('R', R, positive=True) _validate_int('nMic', nMic, positive=True) _validate_int('order_sht', order_sht, positive=True) _validate_int('Lfilt', Lfilt, positive=True, parity='even') _validate_int('fs', fs, positive=True) _validate_float('amp_threshold', amp_threshold) f = np.arange(Lfilt // 2 + 1) * fs / Lfilt # 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) # Modal responses kR = 2 * np.pi * f * R / c bN = sph_modal_coefs(order_sht, kR, 'rigid') / (4 * np.pi) # Encoding matrix with regularization a_dB = amp_threshold alpha = complex(np.sqrt(nMic) * np.power( 10, a_dB / 20)) # Explicit casting to allow negative sqrt (a_dB < 0) beta = np.sqrt( (1 - np.sqrt(1 - 1 / np.power(alpha, 2))) / (1 + np.sqrt(1 - 1 / np.power(alpha, 2)))) # Moreau & Daniel # Regularized single channel equalization filters per order H_filt = (bN).conj() / (np.power(np.abs(bN), 2) + np.power(beta, 2) * np.ones( (Lfilt // 2 + 1, order_sht + 1))) # Time domain filters temp = H_filt.copy() temp[-1, :] = np.real(temp[-1, :]) # Create the symmetric conjugate negative frequency response for a real time-domain signal h_filt = np.real( np.fft.fftshift(np.fft.ifft(np.append(temp, np.conj(temp[-2:0:-1, :]), axis=0), axis=0), axes=0)) # TODO: check return ordering return h_filt, H_filt
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
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
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
def test_validate_int(): # TypeError: not an integer wrong_values = [ '1', True, 2.3, 1e4, 3j, [1], None, np.nan, np.inf, np.asarray([0.5]) ] for wv in wrong_values: with pytest.raises(TypeError, match='must be an instance of int'): _validate_int('vw', wv) # ValueError: not positive wrong_values = [-1, -3] for wv in wrong_values: with pytest.raises(ValueError, match='must be positive'): _validate_int('vw', wv, positive=True) # ValueError: greater than limit limit = 4 wrong_values = [5, 6] for wv in wrong_values: with pytest.raises(ValueError, match='must be smaller than'): _validate_int('vw', wv, limit=limit) # ValueError: wrong parity wrong_values = [1, 3] for wv in wrong_values: with pytest.raises(ValueError, match='must be even'): _validate_int('vw', wv, parity='even') wrong_values = [2, 4] for wv in wrong_values: with pytest.raises(ValueError, match='must be odd'): _validate_int('vw', wv, parity='odd') # Type: wrong parity string wrong_values = [2, 4] for wv in wrong_values: with pytest.raises(TypeError, match='unknown parity value'): _validate_int('vw', wv, parity='asdf')
def simulate_sph_array(N_filt, mic_dirs_rad, src_dirs_rad, arrayType, R, N_order, fs, dirCoef=None): """ Simulate the impulse responses of a spherical 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. Expressed in [azi, ele] pairs. Dimension = (N_mic, C-1). src_dirs_rad: ndarray Direction of arrival of the indicent plane waves, in radians. Expressed in [azi, ele] pairs. Dimension = (N_doa, C-1). arrayType: str 'open', 'rigid' or 'directional'. Target sampling rate R: float Radius of the array sphere, in meter. N_order: int Maximum spherical harmonic expansion order. fs: int Sample rate. dirCoef: float, optional Directivity coefficient of the sensors. Default to None. 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 spherical microphone array for the given directions of incident plane waves. The array type can be either 'open' for omnidirectional microphones in an open setup, 'rigid' for omnidirectional microphones mounted on a sphere, or 'directional' for an open array of first-order directional microphones determined by `dirCoef`. """ _validate_int('N_filt', N_filt, positive=True, parity='even') _validate_ndarray_2D('mic_dirs_rad', mic_dirs_rad, shape1=masp.C - 1) _validate_ndarray_2D('src_dirs_rad', src_dirs_rad, shape1=masp.C - 1) _validate_string('arrayType', arrayType, choices=['open', 'rigid', 'directional']) _validate_float('R', R, positive=True) _validate_int('N_order', N_order, positive=True) _validate_int('fs', fs, positive=True) if arrayType is 'directional': if dirCoef is None: raise ValueError( 'dirCoef must be defined in the directional case.') _validate_float('dirCoef', dirCoef) # 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.sph_modal_coefs(N_order, kR, arrayType, dirCoef) # 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] U_doa = masp.sph2cart( np.column_stack((src_dirs_rad[:, 0], src_dirs_rad[:, 1], np.ones(N_doa)))) U_mic = masp.sph2cart( np.column_stack((mic_dirs_rad[:, 0], mic_dirs_rad[:, 1], np.ones(N_mic)))) 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): cosangle = np.dot(U_mic, U_doa[i, :]) P = np.zeros((N_order + 1, N_mic)) for n in range(N_order + 1): for nm in range(N_mic): # The Legendre polynomial gives the angular dependency Pn = scipy.special.lpmn(n, n, cosangle[nm])[0][0, -1] P[n, nm] = (2 * n + 1) / (4 * np.pi) * Pn h_mic[:, :, i] = np.matmul(b_Nt, P) H_mic[:, :, i] = np.matmul(b_N, P) return h_mic, H_mic
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