def __init__(self, x, y, z, listener_position=None): """ Parameters ---------- x : array_like y : array_like z : array_like listener_position : (3,), cartesian, optional Offset, will be substracted from the loudspeaker positions. """ self.x = utils.asarray_1d(x) self.y = utils.asarray_1d(y) self.z = utils.asarray_1d(z) if listener_position is None: listener_position = [0, 0, 0] self.listener_position = utils.asarray_1d(listener_position) # Listener position as origin self.x -= listener_position[0] self.y -= listener_position[1] self.z -= listener_position[2] # TODO: Better handling of this, e.g. not effective when updating hull: self.listener_position -= self.listener_position _, _, self.d = utils.cart2sph(self.x, self.y, self.z) # amplitude decay exponent self.a = 1 # Triangulation of points _hull = get_hull(self.x, self.y, self.z) self.points = _hull.points self.npoints = _hull.npoints self.nsimplex = _hull.nsimplex self.vertices = _hull.vertices self.simplices = _hull.simplices self.simplices = sort_vertices(self.simplices) self.centroids = calculate_centroids(self) self.face_areas = calculate_face_areas(self) self.face_normals = calculate_face_normals(self) self.vertex_normals = calculate_vertex_normals(self) self.barycenter = calculate_barycenter(self) # All simplices enclosing listener valid per default for rendering self._encloses_listener = check_listener_inside( self, self.listener_position) self.valid_simplices = self._encloses_listener # see 'ambisonics_setup()' self.ambisonics_hull = [] self.kernel_hull = [] self.characteristic_order = None # some checks assert (len(self.d) == self.npoints)
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
def binauralize(self, ls_signals, fs, orientation=(0, 0), hrirs=None): """Create binaural signals that the loudspeaker signals produce on this setup (no delays). Parameters ---------- ls_signals : (L, S) np.ndarray Loudspeaker signals. fs : int orientation : (azi, colat) tuple, optional Listener orientation offset (azimuth, colatitude) in rad. hrirs : sig.HRIRs, optional Returns ------- l_sig : array_like r_sig : array_like """ if hrirs is None: hrirs = IO.load_hrirs(fs) assert (hrirs.fs == fs) ls_signals = np.atleast_2d(ls_signals) assert ls_signals.shape[0] == self.npoints, \ 'Provide signal per loudspeaker!' # distance attenuation relative_position = self.points - \ self.listener_position ls_azi, ls_colat, ls_r = utils.cart2sph(*relative_position.T) ls_signals = np.diag(1 / ls_r**self.a) @ ls_signals # convolve with hrir l_sig = np.zeros(ls_signals.shape[1] + len(hrirs) - 1) r_sig = np.zeros_like(l_sig) for ch, ls_sig in enumerate(ls_signals): if any(abs(ls_sig) > 10e-6): # Gate at -100dB hrir_l, hrir_r = hrirs.nearest_hrirs( ls_azi[ch] - orientation[0], ls_colat[ch] - orientation[1]) # sum all loudspeakers l_sig += signal.convolve(ls_sig, hrir_l) r_sig += signal.convolve(ls_sig, hrir_r) return l_sig, r_sig
blacklist = None ls_setup = IO.load_layout("../data/ls_layouts/Graz.json", listener_position=listener_position) ls_setup.pop_triangles(normal_limit, aperture_limit, opening_limit, blacklist) else: raise ValueError # %% Show setup ls_setup.show() plots.hull_normals(ls_setup) # Test source location src = np.array([1, 0.5, 2.5]) src_azi, src_colat, _ = utils.cart2sph(*src.tolist()) # %% VBAP gains_vbap = decoder.vbap(src, ls_setup) # %% 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)
# version: 3.6.8 # --- # %% import numpy as np import matplotlib.pyplot as plt from spaudiopy import utils, IO, sph, plots, grids # %% Spherical Harmonics Order 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])
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