def mad(F_nm, hull, N_sph=None): """Loudspeaker signals of Mode-Matching Ambisonic Decoder. Parameters ---------- F_nm : ((N_sph+1)**2, S) numpy.ndarray Matrix of spherical harmonics coefficients of spherical function(S). hull : LoudspeakerSetup N_sph : int Decoding order, defaults to hull.characteristic_order. Returns ------- ls_sig : (L, S) numpy.ndarray Loudspeaker L output signal S. References ---------- ch. 4.9.2, Zotter, F., & Frank, M. (2019). Ambisonics. Springer Topics in Signal Processing. Examples -------- .. plot:: :context: close-figs ls_setup = spa.decoder.LoudspeakerSetup(ls_x, ls_y, ls_z) ls_setup.pop_triangles(normal_limit=85, aperture_limit=90, opening_limit=150) spa.plots.decoder_performance(ls_setup, 'MAD') """ if N_sph is None: if hull.characteristic_order: N_sph = hull.characteristic_order else: N_sph = hull.get_characteristic_order() L = hull.npoints N_sph_in = int(np.sqrt(F_nm.shape[0]) - 1) assert (N_sph_in >= N_sph) # for now ls_azi, ls_colat, ls_r = utils.cart2sph(*hull.points.T) Y_ls = sph.sh_matrix(N_sph, ls_azi, ls_colat, SH_type='real') D = (np.linalg.pinv(Y_ls)).T D *= np.sqrt(L / (N_sph + 1)**2) # Energy to unity (on t-design) # loudspeaker output signals ls_sig = D @ F_nm[:(N_sph + 1)**2, :] return ls_sig
# %% Ambisonic decoding # Ambisonic setup N_e = ls_setup.get_characteristic_order() ls_setup.ambisonics_setup(update_hull=True, N_kernel=20) # Show ALLRAP hulls plots.hull(ls_setup.ambisonics_hull, title='Ambisonic hull') plots.hull(ls_setup.kernel_hull, mark_invalid=False, title='Kernel hull') # ALLRAP gains_allrap = decoder.allrap(src, ls_setup, N_sph=N_e) # ALLRAP2 gains_allrap2 = decoder.allrap2(src, ls_setup, N_sph=N_e) # ALLRAD input_F_nm = sph.sh_matrix(N_e, src_azi, src_colat, 'real').T # SH dirac out_allrad = decoder.allrad(input_F_nm, ls_setup, N_sph=N_e) out_allrad2 = decoder.allrad2(input_F_nm, ls_setup, N_sph=N_e) utils.test_diff(gains_allrap, out_allrad, msg="ALLRAD and ALLRAP:") utils.test_diff(gains_allrap2, out_allrad2, msg="ALLRAD2 and ALLRAP2:") # Nearest Loudspeaker gains_nls = decoder.nearest_loudspeaker(src, ls_setup) # %% test multiple sources _grid, _weights = grids.load_Fliege_Maier_nodes(10) G_vbap = decoder.vbap(_grid, ls_setup) G_allrap = decoder.allrap(_grid, ls_setup) G_allrap2 = decoder.allrap2(_grid, ls_setup) G_vbip = decoder.vbip(_grid, ls_setup)
N = 1 # %% # Grids t = grids.load_t_design(2) t_az, t_colat, t_r = utils.cart2sph(t[:, 0], t[:, 1], t[:, 2]) azi = t_az colat = t_colat # %% # First, check condition number to which SH order the SHT is stable # Tetraeder is not suited for SHT N>1: sph.check_cond_sht(3, t_az, t_colat, 'real') # %% Real and Complex SHs Y_nm_c = sph.sh_matrix(N, azi, colat, 'complex') Y_nm_r = sph.sh_matrix(N, azi, colat, 'real') # %% # Look at some SHTs sig = np.array([1, 1, 1, 1]) sig_t = np.c_[np.eye(4), np.eye(4)] # second axis s(t) sig_B = sph.soundfield_to_b(sig) F_nm = sph.sht(sig, N, azi, colat, SH_type='real') F_nm_t = sph.sht(sig_t, N, azi, colat, SH_type='real') # %% F_nm_c = sph.sht(sig, N, azi, colat, SH_type='complex') F_nm_c_t = sph.sht(sig_t, N, azi, colat, SH_type='complex') # %%
def allrad2(F_nm, hull, N_sph=None, jobs_count=1): """Loudspeaker signals of All-Round Ambisonic Decoder 2. Parameters ---------- F_nm : ((N_sph+1)**2, S) numpy.ndarray Matrix of spherical harmonics coefficients of spherical function(S). hull : LoudspeakerSetup N_sph : int Decoding order, defaults to hull.characteristic_order. jobs_count : int or None, optional Number of parallel jobs, 'None' employs 'cpu_count'. Returns ------- ls_sig : (L, S) numpy.ndarray Loudspeaker L output signal S. References ---------- Zotter, F., & Frank, M. (2018). Ambisonic decoding with panning-invariant loudness on small layouts (AllRAD2). In 144th AES Convention. Examples -------- .. plot:: :context: close-figs ls_setup = spa.decoder.LoudspeakerSetup(ls_x, ls_y, ls_z) ls_setup.pop_triangles(normal_limit=85, aperture_limit=90, opening_limit=150) ls_setup.ambisonics_setup(update_hull=True) spa.plots.decoder_performance(ls_setup, 'ALLRAD2') """ if not hull.ambisonics_hull: raise ValueError('Run LoudspeakerSetup.ambisonics_setup() first!') if hull.kernel_hull: kernel_hull = hull.kernel_hull else: raise ValueError('Run LoudspeakerSetup.ambisonics_setup() first!') if N_sph is None: N_sph = hull.characteristic_order N_sph_in = int(np.sqrt(F_nm.shape[0]) - 1) assert (N_sph == N_sph_in) # for now if N_sph_in > kernel_hull.N_kernel: warn("Undersampling the sphere. Needs higher N_Kernel.") # virtual t-design loudspeakers J = len(kernel_hull.points) # virtual speakers expressed as phantom sources (Kernel) G_k = allrap2(src=kernel_hull.points, hull=hull, N_sph=N_sph, jobs_count=jobs_count) # tapering already applied in kernel, sufficient? # virtual Ambisonic decoder _k_azi, _k_colat, _k_r = utils.cart2sph(kernel_hull.points[:, 0], kernel_hull.points[:, 1], kernel_hull.points[:, 2]) # band-limited Dirac Y_bld = sph.sh_matrix(N_sph, _k_azi, _k_colat, SH_type='real') # ALLRAD2 Decoder D = 4 * np.pi / J * G_k.T @ Y_bld # loudspeaker output signals ls_sig = D @ F_nm return ls_sig
def allrad(F_nm, hull, N_sph=None, jobs_count=1): """Loudspeaker signals of All-Round Ambisonic Decoder. Parameters ---------- F_nm : ((N_sph+1)**2, S) numpy.ndarray Matrix of spherical harmonics coefficients of spherical function(S). hull : LoudspeakerSetup N_sph : int Decoding order. jobs_count : int or None, optional Number of parallel jobs, 'None' employs 'cpu_count'. Returns ------- ls_sig : (L, S) numpy.ndarray Loudspeaker L output signal S. References ---------- Zotter, F., & Frank, M. (2012). All-Round Ambisonic Panning and Decoding. Journal of Audio Engineering Society, Sec. 6. Examples -------- .. plot:: :context: close-figs ls_setup = spa.decoder.LoudspeakerSetup(ls_x, ls_y, ls_z) ls_setup.pop_triangles(normal_limit=85, aperture_limit=90, opening_limit=150) ls_setup.ambisonics_setup(update_hull=True) spa.plots.decoder_performance(ls_setup, 'ALLRAD') """ if hull.ambisonics_hull: ambisonics_hull = hull.ambisonics_hull else: raise ValueError('Run LoudspeakerSetup.ambisonics_setup() first!') if hull.kernel_hull: kernel_hull = hull.kernel_hull else: raise ValueError('Run LoudspeakerSetup.ambisonics_setup() first!') if N_sph is None: N_sph = hull.characteristic_order N_sph_in = int(np.sqrt(F_nm.shape[0]) - 1) assert (N_sph == N_sph_in) # for now if N_sph_in > kernel_hull.N_kernel: warn("Undersampling the sphere. Needs higher N_Kernel.") # virtual t-design loudspeakers J = len(kernel_hull.points) # virtual speakers expressed as VBAP phantom sources (Kernel) G_k = vbap(src=kernel_hull.points, hull=ambisonics_hull, jobs_count=jobs_count) # SH tapering coefficients a_n = sph.max_rE_weights(N_sph) a_n = sph.unity_gain(a_n) a_nm = sph.repeat_per_order(a_n) # virtual Ambisonic decoder _k_azi, _k_colat, _k_r = utils.cart2sph(kernel_hull.points[:, 0], kernel_hull.points[:, 1], kernel_hull.points[:, 2]) # band-limited Dirac Y_bld = sph.sh_matrix(N_sph, _k_azi, _k_colat, SH_type='real') # ALLRAD Decoder D = 4 * np.pi / J * G_k.T @ Y_bld # apply tapering to decoder matrix D = D @ np.diag(a_nm) # remove imaginary loudspeakers if ambisonics_hull.imaginary_ls_idx is not None: D = np.delete(D, ambisonics_hull.imaginary_ls_idx, axis=0) # loudspeaker output signals ls_sig = D @ F_nm return ls_sig
def allrap2(src, hull, N_sph=None, jobs_count=1): """Loudspeaker gains for All-Round Ambisonic Panning 2. Parameters ---------- src : (N, 3) Cartesian coordinates of N sources to be rendered. hull : LoudspeakerSetup N_sph : int Decoding order, defaults to hull.characteristic_order. jobs_count : int or None, optional Number of parallel jobs, 'None' employs 'cpu_count'. Returns ------- gains : (N, L) numpy.ndarray Panning gains for L loudspeakers to render N sources. References ---------- Zotter, F., & Frank, M. (2018). Ambisonic decoding with panning-invariant loudness on small layouts (AllRAD2). In 144th AES Convention. Examples -------- .. plot:: :context: close-figs ls_setup = spa.decoder.LoudspeakerSetup(ls_x, ls_y, ls_z) ls_setup.pop_triangles(normal_limit=85, aperture_limit=90, opening_limit=150) ls_setup.ambisonics_setup(update_hull=True) spa.plots.decoder_performance(ls_setup, 'ALLRAP2') """ if hull.ambisonics_hull: ambisonics_hull = hull.ambisonics_hull else: raise ValueError('Run LoudspeakerSetup.ambisonics_setup() first!') if hull.kernel_hull: kernel_hull = hull.kernel_hull else: raise ValueError('Run LoudspeakerSetup.ambisonics_setup() first!') if N_sph is None: N_sph = hull.characteristic_order src = np.atleast_2d(src) assert (src.shape[1] == 3) # normalize direction src = src / np.linalg.norm(src, axis=1)[:, np.newaxis] # virtual t-design loudspeakers J = len(kernel_hull.points) # virtual speakers expressed as VBAP phantom sources (Kernel) G_k = vbap(src=kernel_hull.points, hull=ambisonics_hull, jobs_count=jobs_count) # SH tapering coefficients a_n = sph.max_rE_weights(N_sph) a_n = sph.unity_gain(a_n) # sqrt(E) normalization (eq.6) a_w = np.sqrt( np.sum((2 * (np.arange(N_sph + 1) + 1)) / (4 * np.pi) * a_n**2)) a_n /= a_w a_nm = sph.repeat_per_order(a_n) # sources _s_azi, _s_colat, _s_r = utils.cart2sph(src[:, 0], src[:, 1], src[:, 2]) Y_s = sph.sh_matrix(N_sph, _s_azi, _s_colat, SH_type='real') # kernel _k_azi, _k_colat, _k_r = utils.cart2sph(kernel_hull.points[:, 0], kernel_hull.points[:, 1], kernel_hull.points[:, 2]) Y_k = sph.sh_matrix(N_sph, _k_azi, _k_colat, SH_type='real') # discretized (band-limited) ambisonic panning function G_bld = Y_s @ np.diag(a_nm) @ Y_k.T # remove imaginary loudspeaker if ambisonics_hull.imaginary_ls_idx is not None: G_k = np.delete(G_k, ambisonics_hull.imaginary_ls_idx, axis=1) gains = np.sqrt(4 * np.pi / J * G_bld**2 @ G_k**2) return gains
def epad(F_nm, hull, N_sph=None): r"""Loudspeaker signals of Energy-Preserving Ambisonic Decoder. Parameters ---------- F_nm : ((N_sph+1)**2, S) numpy.ndarray Matrix of spherical harmonics coefficients of spherical function(S). hull : LoudspeakerSetup N_sph : int Decoding order, defaults to hull.characteristic_order. Returns ------- ls_sig : (L, S) numpy.ndarray Loudspeaker L output signal S. Notes ----- Number of loudspeakers should be greater or equal than SH channels, i.e. .. math:: L \geq (N_{sph}+1)^2 . References ---------- Zotter, F., Pomberger, H., & Noisternig, M. (2012). Energy-preserving ambisonic decoding. Acta Acustica United with Acustica, 98(1), 37–47. Examples -------- .. plot:: :context: close-figs ls_setup = spa.decoder.LoudspeakerSetup(ls_x, ls_y, ls_z) ls_setup.pop_triangles(normal_limit=85, aperture_limit=90, opening_limit=150) spa.plots.decoder_performance(ls_setup, 'EPAD') spa.plots.decoder_performance(ls_setup, 'EPAD', N_sph=2, title='$N_{sph}=2$') """ if N_sph is None: if hull.characteristic_order: N_sph = hull.characteristic_order else: N_sph = hull.get_characteristic_order() L = hull.npoints if (L < (N_sph + 1)**2): warn('EPAD needs more loudspeakers for this N_sph!' f' ({L} < {(N_sph+1)**2})') N_sph_in = int(np.sqrt(F_nm.shape[0]) - 1) assert (N_sph_in >= N_sph) # for now # SVD of LS base ls_azi, ls_colat, ls_r = utils.cart2sph(*hull.points.T) Y_ls = sph.sh_matrix(N_sph, ls_azi, ls_colat, SH_type='real') U, S, VH = np.linalg.svd(Y_ls) # Set singular values to identity and truncate S_new = np.eye(L, (N_sph + 1)**2) D = U @ S_new @ VH # Scale to unity D *= np.sqrt(4 * np.pi / L) # Amplitude to unity D *= np.sqrt(L / (N_sph + 1)**2) # Energy to unity # loudspeaker output signals ls_sig = D @ F_nm[:(N_sph + 1)**2, :] return ls_sig