PKmx6GMՑPPsfs/_version.py"""Version of the SFS Toolbox. This is the only place where the version number is stored. During installation, this file is read by ../setup.py and when importing the 'sfs' module, this module is imported by __init__.py. Whenever the version is incremented, a Git tag with the same name should be created. """ __version__ = "0.1.0" PKrqE!Zssfs/__init__.py"""Sound Field Synthesis Toolbox.""" from ._version import __version__ from . import tapering from . import array from . import util from . import defs try: from . import plot except ImportError: pass from . import mono PKs6G+A5A5 sfs/array.py"""Compute positions of various secondary source distributions. .. plot:: :context: reset import sfs import matplotlib.pyplot as plt plt.rcParams['figure.figsize'] = 8, 4 # inch plt.rcParams['axes.grid'] = True """ from __future__ import division # for Python 2.x import numpy as np from . import util def linear(N, spacing, center=[0, 0, 0], n0=[1, 0, 0]): """Linear secondary source distribution. Parameters ---------- N : int Number of loudspeakers. spacing : float Distance (in metres) between loudspeakers. center : (3,) array_like, optional Coordinates of array center. n0 : (3,) array_like, optional Normal vector of array. Returns ------- positions : (N, 3) numpy.ndarray Positions of secondary sources directions : (N, 3) numpy.ndarray Orientations (normal vectors) of secondary sources weights : (N,) numpy.ndarray Weights of secondary sources Example ------- .. plot:: :context: close-figs x0, n0, a0 = sfs.array.linear(16, 0.2, n0=[0, -1, 0]) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') """ positions = np.zeros((N, 3)) positions[:, 1] = (np.arange(N) - N/2 + 1/2) * spacing positions, directions = _rotate_array(positions, [1, 0, 0], [1, 0, 0], n0) positions += center directions = np.tile(directions, (N, 1)) weights = spacing * np.ones(N) return positions, directions, weights def linear_nested(N, dx1, dx2, center=[0, 0, 0], n0=[1, 0, 0]): """Nested linear secondary source distribution. Example ------- .. plot:: :context: close-figs x0, n0, a0 = sfs.array.linear_nested(16, 0.15, 0.2, n0=[0, -1, 0]) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') """ # first segment x00, n00, a00 = linear(N//3, dx2, center=[0, -N//6*(dx1+dx2), 0]) positions = x00 directions = n00 # second segment x00, n00, a00 = linear(N//3, dx1) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) # third segment x00, n00, a00 = linear(N//3, dx2, center=[0, N//6*(dx1+dx2), 0]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) # compute weights weights = weights_linear(positions) # shift and rotate array positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) positions += center return positions, directions, weights def linear_random(N, dy1, dy2, center=[0, 0, 0], n0=[1, 0, 0]): """Randomly sampled linear array. Example ------- .. plot:: :context: close-figs x0, n0, a0 = sfs.array.linear_random(12, 0.15, 0.4, n0=[0, -1, 0]) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') """ # vector of uniformly distributed random distances between dy2 > dy1 dist = dy1 + (dy2-dy1)*np.random.rand(N-1) # positions of secondary sources positions = np.zeros((N, 3)) for m in range(1, N): positions[m, 1] = positions[m-1, 1] + dist[m-1] # weights of secondary sources weights = weights_linear(positions) # directions of secondary sources directions = np.tile([1, 0, 0], (N, 1)) # shift array to origin positions[:, 1] -= positions[-1, 1] / 2 # shift and rotate array positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) positions += center return positions, directions, weights def circular(N, R, center=[0, 0, 0]): """Circular secondary source distribution parallel to the xy-plane. Example ------- .. plot:: :context: close-figs x0, n0, a0 = sfs.array.circular(16, 1) sfs.plot.loudspeaker_2d(x0, n0, a0, size=0.2, show_numbers=True) plt.axis('equal') """ center = util.asarray_1d(center, dtype=np.float64) positions = np.tile(center, (N, 1)) alpha = np.linspace(0, 2 * np.pi, N, endpoint=False) positions[:, 0] += R * np.cos(alpha) positions[:, 1] += R * np.sin(alpha) directions = np.zeros_like(positions) directions[:, 0] = np.cos(alpha + np.pi) directions[:, 1] = np.sin(alpha + np.pi) weights = 2 * np.pi * R / N * np.ones(N) return positions, directions, weights def rectangular(Nx, dx, Ny, dy, center=[0, 0, 0], n0=None): """Rectangular secondary source distribution. Example ------- .. plot:: :context: close-figs x0, n0, a0 = sfs.array.rectangular(8, 0.2, 4, 0.2) sfs.plot.loudspeaker_2d(x0, n0, a0, show_numbers=True) plt.axis('equal') """ # left array x00, n00, a00 = linear(Ny, dy) positions = x00 directions = n00 weights = a00 # upper array x00, n00, a00 = linear(Nx, dx, center=[Nx/2 * dx, x00[-1, 1] + dy/2, 0], n0=[0, -1, 0]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # right array x00, n00, a00 = linear(Ny, dy, center=[x00[-1, 0] + dx/2, 0, 0], n0=[-1, 0, 0]) x00 = np.flipud(x00) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # lower array x00, n00, a00 = linear(Nx, dx, center=[Nx/2 * dx, x00[-1, 1] - dy/2, 0], n0=[0, 1, 0]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # shift array to origin positions -= [Nx/2 * dx, 0, 0] # rotate array if n0 is not None: positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) # shift array to desired position positions += center return positions, directions, weights def rounded_edge(Nxy, Nr, dx, center=[0, 0, 0], n0=None): """Array along the xy-axis with rounded edge at the origin. Parameters ---------- Nxy : int Number of secondary sources along x- and y-axis. Nr : int Number of secondary sources in rounded edge. Radius of edge is adjusted to equdistant sampling along entire array. center : (3,) array_like, optional Position of edge. n0 : (3,) array_like, optional Normal vector of array. Default orientation is along xy-axis. Returns ------- positions : (N, 3) numpy.ndarray Positions of secondary sources directions : (N, 3) numpy.ndarray Orientations (normal vectors) of secondary sources weights : (N,) numpy.ndarray Integration weights of secondary sources Example ------- .. plot:: :context: close-figs x0, n0, a0 = sfs.array.rounded_edge(8, 5, 0.2) sfs.plot.loudspeaker_2d(x0, n0, a0) plt.axis('equal') """ # radius of rounded edge Nr += 1 R = 2/np.pi * Nr * dx # array along y-axis x00, n00, a00 = linear(Nxy, dx, center=[0, Nxy//2*dx+dx/2+R, 0]) x00 = np.flipud(x00) positions = x00 directions = n00 weights = a00 # round part x00 = np.zeros((Nr, 3)) n00 = np.zeros((Nr, 3)) a00 = np.zeros(Nr) for n in range(0, Nr): alpha = np.pi/2 * n/Nr x00[n, 0] = R * (1-np.cos(alpha)) x00[n, 1] = R * (1-np.sin(alpha)) n00[n, 0] = np.cos(alpha) n00[n, 1] = np.sin(alpha) a00[n] = dx positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # array along x-axis x00, n00, a00 = linear(Nxy, dx, center=[Nxy//2*dx-dx/2+R, 0, 0], n0=[0, 1, 0]) x00 = np.flipud(x00) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # rotate array if n0 is not None: positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) # shift array to desired position positions += center return positions, directions, weights def planar(Ny, dy, Nz, dz, center=[0, 0, 0], n0=None): """Planar secondary source distribtion.""" # initialize vectors for later np.concatenate positions = np.zeros((1, 3)) directions = np.zeros((1, 3)) for z in (np.arange(Nz) - Nz / 2 + 1 / 2) * dz: x00, n00, a00 = linear(Ny, dy, center=[0, 0, z]) positions = np.concatenate((positions, x00), axis=0) directions = np.concatenate((directions, n00), axis=0) # remove first element from initialization positions = np.delete(positions, 0, axis=0) directions = np.delete(directions, 0, axis=0) weights = dy * dz * np.ones(Ny*Nz) # rotate array if n0 is not None: positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) # shift array to desired position positions += center return positions, directions, weights def cube(Nx, dx, Ny, dy, Nz, dz, center=[0, 0, 0], n0=None): """Cube shaped secondary source distribtion.""" # left array x00, n00, a00 = planar(Ny, dy, Nz, dz) positions = x00 directions = n00 weights = a00 # upper array x00, n00, a00 = planar(Nx, dx, Nz, dz, center=[Nx/2 * dx, x00[-1, 1] + dy/2, 0], n0=[0, -1, 0]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # right array x00, n00, a00 = planar(Ny, dy, Nz, dz, center=[x00[-1, 0] + dx/2, 0, 0], n0=[-1, 0, 0]) x00 = np.flipud(x00) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # lower array x00, n00, a00 = planar(Nx, dx, Nz, dz, center=[Nx/2 * dx, x00[-1, 1] - dy/2, 0], n0=[0, 1, 0]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # bottom array x00, n00, a00 = planar(Nx, dx, Ny, dy, center=[Nx/2 * dx, 0, -Nz/2 * dz], n0=[0, 0, 1]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # top array x00, n00, a00 = planar(Nx, dx, Ny, dy, center=[Nx/2 * dx, 0, Nz/2 * dz], n0=[0, 0, -1]) positions = np.concatenate((positions, x00)) directions = np.concatenate((directions, n00)) weights = np.concatenate((weights, a00)) # shift array to origin positions -= [Nx/2 * dx, 0, 0] # rotate array if n0 is not None: positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) # shift array to desired position positions += center return positions, directions, weights def sphere_load(fname, radius, center=[0, 0, 0]): """Spherical secondary source distribution loaded from datafile. ASCII Format (see MATLAB SFS Toolbox) with 4 numbers (3 position, 1 weight) per secondary source located on the unit circle. """ x0 = np.loadtxt(fname) weights = x0[:, 3] directions = -x0[:, :3] positions = center + radius * x0[:, :3] return positions, directions, weights def load(fname, center=[0, 0, 0], n0=None): """Load secondary source positions from datafile. Comma Seperated Values (CSV) format with 7 values (3 positions, 3 directions, 1 weight) per secondary source. """ data = np.loadtxt(fname, delimiter=',') positions = data[:, [0, 1, 2]] directions = data[:, [3, 4, 5]] weights = np.squeeze(data[:, [6]]) # rotate array if n0 is not None: positions, directions = _rotate_array(positions, directions, [1, 0, 0], n0) # shift array to desired position positions += center return positions, directions, weights def weights_linear(positions): """Calculate loudspeaker weights for a linear array. The linear array has to be parallel to the y-axis. """ N = len(positions) weights = np.zeros(N) dy = np.diff(positions[:, 1]) weights[0] = dy[0] for m in range(1, N - 1): weights[m] = 0.5 * (dy[m-1] + dy[m]) weights[-1] = dy[-1] return np.abs(weights) def weights_closed(positions): """Calculate loudspeaker weights for a simply connected array. The weights are calculated according to the midpoint rule. Note: The loudspeaker positions have to be ordered on the closed contour. """ positions = util.asarray_of_rows(positions) if len(positions) == 0: weights = [] elif len(positions) == 1: weights = [1.0] else: successors = np.roll(positions, -1, axis=0) d = [np.linalg.norm(b - a) for a, b in zip(positions, successors)] weights = [0.5 * (a + b) for a, b in zip(d, d[-1:] + d)] return np.array(weights) def _rotate_array(x0, n0, n1, n2): """Rotate secondary sources from n1 to n2.""" R = util.rotation_matrix(n1, n2) x0 = np.inner(x0, R) n0 = np.inner(n0, R) return x0, n0 PKs6G+ sfs/util.py"""Various utility functions.""" import numpy as np from . import defs def rotation_matrix(n1, n2): """Compute rotation matrix for rotation from `n1` to `n2`.""" n1 = asarray_1d(n1) n2 = asarray_1d(n2) # no rotation required if all(n1 == n2): return np.eye(3) v = np.cross(n1, n2) s = np.linalg.norm(v) # check for rotation of 180deg around one axis if s == 0: rot = np.identity(3) for i in np.arange(3): if np.abs(n1[i]) > 0 and np.abs(n1[i]) > 0 and n1[i] == -n2[i]: rot[i, i] = -1 return rot c = np.inner(n1, n2) vx = [[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]] return np.identity(3) + vx + np.dot(vx, vx) * (1 - c) / s ** 2 def wavenumber(omega, c=None): """Compute the wavenumber for a given radial frequency.""" if c is None: c = defs.c return omega / c def direction_vector(alpha, beta=np.pi/2): """Compute normal vector from azimuth, colatitude.""" return sph2cart(alpha, beta, 1) def sph2cart(alpha, beta, r): """Spherical to cartesian coordinates.""" x = r * np.cos(alpha) * np.sin(beta) y = r * np.sin(alpha) * np.sin(beta) z = r * np.cos(beta) return x, y, z def cart2sph(x, y, z): """Cartesian to spherical coordinates.""" alpha = np.arctan2(y, x) beta = np.arccos(z / np.sqrt(x**2 + y**2)) r = np.sqrt(x**2 + y**2 + z**2) return alpha, beta, r def asarray_1d(a, **kwargs): """Squeeze the input and check if the result is one-dimensional. Returns `a` converted to a :class:`numpy.ndarray` and stripped of all singleton dimensions. Scalars are "upgraded" to 1D arrays. The result must have exactly one dimension. If not, an error is raised. """ result = np.squeeze(np.asarray(a, **kwargs)) if result.ndim == 0: result = result.reshape((1,)) elif result.ndim > 1: raise ValueError("array must be one-dimensional") return result def asarray_of_rows(a, **kwargs): """Convert to 2D array, turn column vector into row vector. Returns `a` converted to a :class:`numpy.ndarray` and stripped of all singleton dimensions. If the result has exactly one dimension, it is re-shaped into a 2D row vector. """ result = np.squeeze(np.asarray(a, **kwargs)) if result.ndim == 1: result = result.reshape(1, -1) return result def asarray_of_arrays(a, **kwargs): """Convert the input to an array consisting of arrays. A one-dimensional :class:`numpy.ndarray` with `dtype=object` is returned, containing the elements of `a` as :class:`numpy.ndarray` (whose `dtype` and other options can be specified with `**kwargs`). """ result = np.empty(len(a), dtype=object) for i, element in enumerate(a): result[i] = np.asarray(element, **kwargs) return result def strict_arange(start, stop, step=1, endpoint=False, dtype=None, **kwargs): """Like :func:`numpy.arange`, but compensating numeric errors. Unlike :func:`numpy.arange`, but similar to :func:`numpy.linspace`, providing `endpoint=True` includes both endpoints. Parameters ---------- start, stop, step, dtype See :func:`numpy.arange`. endpoint See :func:`numpy.linspace`. .. note:: With `endpoint=True`, the difference between `start` and `end` value must be an integer multiple of the corresponding `spacing` value! **kwargs All further arguments are forwarded to :func:`numpy.isclose`. Returns ------- numpy.ndarray Array of evenly spaced values. See :func:`numpy.arange`. """ remainder = (stop - start) % step if np.any(np.isclose(remainder, (0.0, step), **kwargs)): if endpoint: stop += step * 0.5 else: stop -= step * 0.5 elif endpoint: raise ValueError("Invalid stop value for endpoint=True") return np.arange(start, stop, step, dtype) def xyz_grid(x, y, z, spacing, endpoint=True, **kwargs): """Create a grid with given range and spacing. Parameters ---------- x, y, z : float or pair of float Inclusive range of the respective coordinate or a single value if only a slice along this dimension is needed. spacing : float or triple of float Grid spacing. If a single value is specified, it is used for all dimensions, if multiple values are given, one value is used per dimension. If a dimension (`x`, `y` or `z`) has only a single value, the corresponding spacing is ignored. endpoint : bool, optional If ``True`` (the default), the endpoint of each range is included in the grid. Use ``False`` to get a result similar to :func:`numpy.arange`. See :func:`strict_arange`. **kwargs All further arguments are forwarded to :func:`strict_arange`. Returns ------- list of numpy.ndarrays A grid that can be used for sound field calculations. See Also -------- strict_arange, numpy.meshgrid """ if np.isscalar(spacing): spacing = [spacing] * 3 args = [] for i, coord in enumerate([x, y, z]): if np.isscalar(coord): args.append(coord) else: start, stop = coord args.append(strict_arange(start, stop, spacing[i], endpoint=endpoint, **kwargs)) return np.meshgrid(*args, sparse=True, copy=False) def normalize(p, grid, xnorm): """Normalize sound field wrt position `xnorm`.""" return p / level(p, grid, xnorm) def level(p, grid, x): """Determine level at position `x` in the sound field `p`.""" x = asarray_1d(x) r = np.linalg.norm(grid - x) idx = np.unravel_index(r.argmin(), r.shape) # p is normally squeezed, therefore we need only 2 dimensions: idx = idx[:p.ndim] return abs(p[idx]) def broadcast_zip(*args): """Broadcast arguments to the same shape and then use :func:`zip`.""" return zip(*np.broadcast_arrays(*args)) PKX}5GK sfs/plot.py"""Plot sound fields etc.""" import matplotlib.pyplot as plt from matplotlib.patches import PathPatch from matplotlib.path import Path from matplotlib.collections import PatchCollection from mpl_toolkits.mplot3d import Axes3D import numpy as np from . import util def _register_coolwarm_clip(): """Create color map with "over" and "under" values.""" from matplotlib.colors import LinearSegmentedColormap # The 'coolwarm' colormap is based on the paper # "Diverging Color Maps for Scientific Visualization" by Kenneth Moreland # http://www.sandia.gov/~kmorel/documents/ColorMaps/ cdict = plt.cm.datad['coolwarm'] cmap = LinearSegmentedColormap('coolwarm_clip', cdict) cmap.set_over(color=cmap(1.0), alpha=0.7) cmap.set_under(color=cmap(0.0), alpha=0.7) plt.cm.register_cmap(cmap=cmap) _register_coolwarm_clip() del _register_coolwarm_clip def virtualsource_2d(xs, ns=None, type='point', ax=None): """Draw position/orientation of virtual source.""" xs = np.asarray(xs) ns = np.asarray(ns) if ax is None: ax = plt.axes() if type == 'point': vps = plt.Circle(xs, .05, edgecolor='k', facecolor='k') ax.add_artist(vps) for n in range(1, 3): vps = plt.Circle(xs, .05+n*0.05, edgecolor='k', fill=False) ax.add_artist(vps) elif type == 'plane': ns = 0.2 * ns ax.arrow(xs[0], xs[1], ns[0], ns[1], head_width=0.05, head_length=0.1, fc='k', ec='k') def reference_2d(xref, size=0.1, ax=None): """Draw reference/normalization point.""" xref = np.asarray(xref) if ax is None: ax = plt.axes() ax.plot((xref[0]-size, xref[0]+size), (xref[1]-size, xref[1]+size), 'k-') ax.plot((xref[0]-size, xref[0]+size), (xref[1]+size, xref[1]-size), 'k-') def secondarysource_2d(x0, n0, grid=None): """Simple plot of secondary source locations.""" x0 = np.asarray(x0) n0 = np.asarray(n0) ax = plt.axes() # plot only secondary sources inside simulated area if grid is not None: x0, n0 = _visible_secondarysources_2d(x0, n0, grid) # plot symbols for x00 in x0: ss = plt.Circle(x00[0:2], .05, edgecolor='k', facecolor='k') ax.add_artist(ss) def loudspeaker_2d(x0, n0, a0=0.5, size=0.08, show_numbers=False, grid=None, ax=None): """Draw loudspeaker symbols at given locations and angles. Parameters ---------- x0 : (N, 3) array_like Loudspeaker positions. n0 : (N, 3) or (3,) array_like Normal vector(s) of loudspeakers. a0 : float or (N,) array_like, optional Weighting factor(s) of loudspeakers. size : float, optional Size of loudspeakers in metres. show_numbers : bool, optional If ``True``, loudspeaker numbers are shown. grid : triple of numpy.ndarray, optional If specified, only loudspeakers within the `grid` are shown. ax : Axes object, optional The loudspeakers are plotted into this :class:`~matplotlib.axes.Axes` object or -- if not specified -- into the current axes. """ x0 = util.asarray_of_rows(x0) n0 = util.asarray_of_rows(n0) a0 = util.asarray_1d(a0).reshape(-1, 1) # plot only secondary sources inside simulated area if grid is not None: x0, n0 = _visible_secondarysources_2d(x0, n0, grid) # normalized coordinates of loudspeaker symbol (see IEC 60617-9) codes, coordinates = zip(*( (Path.MOVETO, [-0.62, 0.21]), (Path.LINETO, [-0.31, 0.21]), (Path.LINETO, [0, 0.5]), (Path.LINETO, [0, -0.5]), (Path.LINETO, [-0.31, -0.21]), (Path.LINETO, [-0.62, -0.21]), (Path.CLOSEPOLY, [0, 0]), (Path.MOVETO, [-0.31, 0.21]), (Path.LINETO, [-0.31, -0.21]), )) coordinates = np.column_stack([coordinates, np.zeros(len(coordinates))]) coordinates *= size patches = [] for x00, n00 in util.broadcast_zip(x0, n0): # rotate and translate coordinates R = util.rotation_matrix([1, 0, 0], n00) transformed_coordinates = np.inner(coordinates, R) + x00 patches.append(PathPatch(Path(transformed_coordinates[:, :2], codes))) # add collection of patches to current axis p = PatchCollection(patches, edgecolor='0', facecolor=np.tile(1 - a0, 3)) if ax is None: ax = plt.gca() ax.add_collection(p) if show_numbers: for idx, (x00, n00) in enumerate(util.broadcast_zip(x0, n0)): x, y = x00[:2] - 1.2 * size * n00[:2] ax.text(x, y, idx + 1, horizontalalignment='center', verticalalignment='center') def _visible_secondarysources_2d(x0, n0, grid): """Determine secondary sources which lie within `grid`.""" grid = util.asarray_of_arrays(grid) x, y = grid[:2] idx = np.where((x0[:, 0] > x.min()) & (x0[:, 0] < x.max()) & (x0[:, 1] > y.min()) & (x0[:, 1] < x.max())) idx = np.squeeze(idx) return x0[idx, :], n0[idx, :] def loudspeaker_3d(x0, n0, a0=None, w=0.08, h=0.08): """Plot positions and normals of a 3D secondary source distribution.""" fig = plt.figure(figsize=(15, 15)) ax = fig.add_subplot(111, projection='3d') ax.quiver(x0[:, 0], x0[:, 1], x0[:, 2], n0[:, 0], n0[:, 1], n0[:, 2], length=0.1) plt.xlabel('x (m)') plt.ylabel('y (m)') plt.title('Secondary Sources') fig.show() def soundfield(p, grid, xnorm=None, colorbar=True, cmap='coolwarm_clip', ax=None, xlabel='x (m)', ylabel='y (m)', vmax=2.0, vmin=-2.0, **kwargs): """Two-dimensional plot of sound field.""" grid = util.asarray_of_arrays(grid) # normalize sound field wrt xnorm if xnorm is not None: p = util.normalize(p, grid, xnorm) x, y = grid[:2] # ignore z-component if ax is None: ax = plt.gca() im = ax.imshow(np.real(p), cmap=cmap, origin='lower', extent=[x.min(), x.max(), y.min(), y.max()], vmax=vmax, vmin=vmin, aspect='equal', **kwargs) ax.set_adjustable('box-forced') # avoid empty space btw. axis and image if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) if colorbar: ax.figure.colorbar(im, ax=ax) return im def level(p, grid, xnorm=None, colorbar=True, cmap='coolwarm_clip', ax=None, xlabel='x (m)', ylabel='y (m)', vmax=3.0, vmin=-50, **kwargs): """Two-dimensional plot of level (dB) of sound field.""" # normalize sound field wrt xnorm if xnorm is not None: p = util.normalize(p, grid, xnorm) xnorm = None im = soundfield(20*np.log10(np.abs(p)), grid, xnorm, colorbar, cmap, ax, xlabel, ylabel, vmax, vmin, **kwargs) return im PKX}5G]y sfs/defs.py"""Definition of constants.""" # speed of sound c = 343 # tolerance used for secondary source selection selection_tolerance = 1e-6 PKhEVrsfs/tapering.py"""Weights (tapering) for the driving function.""" # from scipy import signal import numpy as np def none(active): """No tapering window.""" return np.asarray(active, dtype=np.float64) def kaiser(active): """Kaiser tapering window.""" idx = _windowidx(active) # compute coefficients window = np.zeros(active.shape) window[idx] = np.kaiser(len(idx), 2) return window def tukey(active, alpha): """Tukey tapering window.""" idx = _windowidx(active) # alpha out of limits if alpha <= 0 or alpha >= 1: return none(active) # design Tukey window x = np.linspace(0, 1, len(idx)+2) w = np.ones(x.shape) first_condition = x < alpha/2 w[first_condition] = 0.5 * (1 + np.cos(2*np.pi/alpha * (x[first_condition] - alpha/2))) third_condition = x >= (1 - alpha/2) w[third_condition] = 0.5 * (1 + np.cos(2*np.pi/alpha * (x[third_condition] - 1 + alpha/2))) # fit window into tapering function window = np.zeros(active.shape) window[idx] = w[1:-1] return window def _windowidx(active): """Returns list of connected indices for window function.""" active = np.asarray(active, dtype=np.float64) # find index were active loudspeakers begin (works for connected contours) if (active[0] == 1 and active[-1] == 0) or np.all(active): a0 = 0 else: a0 = np.argmax(np.diff(active)) + 1 # shift generic index vector to get a connected list of indices idx = np.roll(np.arange(len(active)), -a0) # remove indices of inactive secondary sources idx = idx[0:len(np.squeeze(np.where(active == 1)))] return idx PKy6GH&&#sfs-0.1.0.dist-info/DESCRIPTION.rstSound Field Synthesis Toolbox for Python ======================================== Python implementation of the `Sound Field Synthesis Toolbox`_. .. _Sound Field Synthesis Toolbox: http://github.com/sfstoolbox/sfs/ Documentation: http://sfs.rtfd.org/ Code: http://github.com/sfstoolbox/sfs-python/ Python Package Index: http://pypi.python.org/pypi/sfs/ Requirements ------------ Obviously, you'll need Python_. We normally use Python 3.x, but it *should* also work with Python 2.x. NumPy_ and SciPy_ are needed for the calcuations. If you also want to plot the resulting sound fields, you'll need matplotlib_. Instead of installing all of them separately, you should probably get a Python distribution that already includes everything, e.g. Anaconda_. .. _Python: http://www.python.org/ .. _NumPy: http://www.numpy.org/ .. _SciPy: http://www.scipy.org/scipylib/ .. _matplotlib: http://matplotlib.org/ .. _Anaconda: http://docs.continuum.io/anaconda/ Installation ------------ Once you have installed the above-mentioned dependencies, you can use pip_ to download and install the latest release with a single command:: pip install sfs --user If you want to install it system-wide for all users (assuming you have the necessary rights), you can just drop the ``--user`` option. To un-install, use:: pip uninstall sfs If you want to keep up with the latest and greatest development version you should get it from Github_:: git clone https://github.com/sfstoolbox/sfs-python.git cd sfs-python python setup.py develop --user .. _pip: http://www.pip-installer.org/en/latest/installing.html .. _Github: http://github.com/sfstoolbox/sfs-python/ How to Get Started ------------------ Various examples are located in the directory examples/ * sound_field_synthesis.py: Illustrates the general usage of the toolbox * horizontal_plane_arrays.py: Computes the sound fields for various techniques, virtual sources and loudspeaker array configurations * soundfigures.py: Illustrates the synthesis of sound figures with Wave Field Synthesis PKy6G!sfs-0.1.0.dist-info/metadata.json{"classifiers": ["Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Scientific/Engineering"], "extensions": {"python.details": {"contacts": [{"email": "sfstoolbox@gmail.com", "name": "SFS Toolbox Developers", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/sfstoolbox/"}}}, "extras": [], "generator": "bdist_wheel (0.24.0)", "keywords": ["audio", "SFS", "WFS", "Ambisonics"], "license": "MIT", "metadata_version": "2.0", "name": "sfs", "platform": "any", "run_requires": [{"requires": ["numpy", "scipy"]}], "summary": "Sound Field Synthesis Toolbox", "test_requires": [{"requires": ["pytest"]}], "version": "0.1.0"}PKy6GפS!sfs-0.1.0.dist-info/top_level.txtsfs PKy6G3onnsfs-0.1.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.24.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PKy6GM5 5 sfs-0.1.0.dist-info/METADATAMetadata-Version: 2.0 Name: sfs Version: 0.1.0 Summary: Sound Field Synthesis Toolbox Home-page: http://github.com/sfstoolbox/ Author: SFS Toolbox Developers Author-email: sfstoolbox@gmail.com License: MIT Keywords: audio,SFS,WFS,Ambisonics Platform: any Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Scientific/Engineering Requires-Dist: numpy Requires-Dist: scipy Sound Field Synthesis Toolbox for Python ======================================== Python implementation of the `Sound Field Synthesis Toolbox`_. .. _Sound Field Synthesis Toolbox: http://github.com/sfstoolbox/sfs/ Documentation: http://sfs.rtfd.org/ Code: http://github.com/sfstoolbox/sfs-python/ Python Package Index: http://pypi.python.org/pypi/sfs/ Requirements ------------ Obviously, you'll need Python_. We normally use Python 3.x, but it *should* also work with Python 2.x. NumPy_ and SciPy_ are needed for the calcuations. If you also want to plot the resulting sound fields, you'll need matplotlib_. Instead of installing all of them separately, you should probably get a Python distribution that already includes everything, e.g. Anaconda_. .. _Python: http://www.python.org/ .. _NumPy: http://www.numpy.org/ .. _SciPy: http://www.scipy.org/scipylib/ .. _matplotlib: http://matplotlib.org/ .. _Anaconda: http://docs.continuum.io/anaconda/ Installation ------------ Once you have installed the above-mentioned dependencies, you can use pip_ to download and install the latest release with a single command:: pip install sfs --user If you want to install it system-wide for all users (assuming you have the necessary rights), you can just drop the ``--user`` option. To un-install, use:: pip uninstall sfs If you want to keep up with the latest and greatest development version you should get it from Github_:: git clone https://github.com/sfstoolbox/sfs-python.git cd sfs-python python setup.py develop --user .. _pip: http://www.pip-installer.org/en/latest/installing.html .. _Github: http://github.com/sfstoolbox/sfs-python/ How to Get Started ------------------ Various examples are located in the directory examples/ * sound_field_synthesis.py: Illustrates the general usage of the toolbox * horizontal_plane_arrays.py: Computes the sound fields for various techniques, virtual sources and loudspeaker array configurations * soundfigures.py: Illustrates the synthesis of sound figures with Wave Field Synthesis PKy6Gtsfs-0.1.0.dist-info/RECORDsfs-0.1.0.dist-info/DESCRIPTION.rst,sha256=hPOr7309iWWvJCttPmjfbrJKxoiwOz8TpRkSUb4OBgI,2086 sfs-0.1.0.dist-info/WHEEL,sha256=AvR0WeTpDaxT645bl5FQxUK6NPsTls2ttpcGJg3j1Xg,110 sfs-0.1.0.dist-info/top_level.txt,sha256=6mBnHRnczkH-3MoFY3vtAo_cer661XzjlrnrFOWAW28,4 sfs-0.1.0.dist-info/metadata.json,sha256=l81Oi2lEO7oouKPjJFNB_3aQwE4VEyVJ5w3y546-bXg,793 sfs-0.1.0.dist-info/RECORD,, sfs-0.1.0.dist-info/METADATA,sha256=1QQDefuDXcfBCZOm-8fCHVMH2oX5opdUd1jC93nbCAw,2613 sfs/_version.py,sha256=49Irg6qp3uTeMyjH3O_eL_MtH238KKfUW_d2X1GZDP0,336 sfs/__init__.py,sha256=lywZmmKB_pU0FtzpQS-SJvQIU9em17t4KgsobpSRnAk,231 sfs/array.py,sha256=6G_p7vJf2lA6ZH2FM5NlyiB0VAPRw5PNRyNFm09k7BM,13633 sfs/util.py,sha256=JfEWfhOiyXKtcqKdyNPyt7cWnzwgtMM95GQu6ZB46Y8,6111 sfs/plot.py,sha256=G9-Ui4LQ_cFTw-letauZ2a0WEj_veMmqobelO9jFJ5E,6851 sfs/defs.py,sha256=KmMPT08xAECLTujlUsOxxHAnZcGZ6-XxwmvLBJjVkaY,133 sfs/tapering.py,sha256=rK4pghz8hOO_WgDvk-665v9nLHf6-7sLDzrdt0LBxIA,1708 PKmx6GMՑPPsfs/_version.pyPKrqE!Zs}sfs/__init__.pyPKs6G+A5A5 sfs/array.pyPKs6G+ 7sfs/util.pyPKX}5GK Psfs/plot.pyPKX}5G]y jsfs/defs.pyPKhEVrksfs/tapering.pyPKy6GH&&#wrsfs-0.1.0.dist-info/DESCRIPTION.rstPKy6G!zsfs-0.1.0.dist-info/metadata.jsonPKy6GפS!6~sfs-0.1.0.dist-info/top_level.txtPKy6G3onny~sfs-0.1.0.dist-info/WHEELPKy6GM5 5 sfs-0.1.0.dist-info/METADATAPKy6Gtsfs-0.1.0.dist-info/RECORDPK d