PKdpHG'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.1" 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 PK2dHG+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 PK2dHG+ 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)) PKtEGK 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 PKtEG]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 PKU%FVWMggsfs/mono/__init__.pyfrom . import drivingfunction from . import source from . import synthesized from . import soundfigure PKtEG=ݺ " "sfs/mono/drivingfunction.py"""Compute driving functions for various systems.""" import numpy as np from numpy.core.umath_tests import inner1d # element-wise inner product from scipy.special import hankel2 from scipy.special import sph_jn, sph_yn from .. import util from .. import defs def wfs_2d_line(omega, x0, n0, xs, c=None): """Line source by 2-dimensional WFS. :: D(x0,k) = j k (x0-xs) n0 / |x0-xs| * H1(k |x0-xs|) """ x0 = np.asarray(x0) n0 = np.asarray(n0) xs = np.squeeze(np.asarray(xs)) k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) return 1j * k * inner1d(ds, n0) / r * hankel2(1, k * r) def _wfs_point(omega, x0, n0, xs, c=None): """Point source by two- or three-dimensional WFS. :: (x0-xs) n0 D(x0,k) = j k ------------- e^(-j k |x0-xs|) |x0-xs|^(3/2) """ x0 = np.asarray(x0) n0 = np.asarray(n0) xs = np.squeeze(np.asarray(xs)) k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) return 1j * k * inner1d(ds, n0) / r ** (3 / 2) * np.exp(-1j * k * r) wfs_2d_point = _wfs_point def wfs_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None): """Point source by 2.5-dimensional WFS. :: ____________ (x0-xs) n0 D(x0,k) = \|j k |xref-x0| ------------- e^(-j k |x0-xs|) |x0-xs|^(3/2) """ x0 = np.asarray(x0) n0 = np.asarray(n0) xs = np.squeeze(np.asarray(xs)) xref = np.squeeze(np.asarray(xref)) k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) return wfs_25d_preeq(omega, omalias, c) * \ np.sqrt(np.linalg.norm(xref - x0)) * inner1d(ds, n0) / \ r ** (3 / 2) * np.exp(-1j * k * r) wfs_3d_point = _wfs_point def _wfs_plane(omega, x0, n0, n=[0, 1, 0], c=None): """Plane wave by two- or three-dimensional WFS. Eq.(17) from [Spors et al, 2008]:: D(x0,k) = j k n n0 e^(-j k n x0) """ x0 = np.asarray(x0) n0 = np.asarray(n0) n = np.squeeze(np.asarray(n)) k = util.wavenumber(omega, c) return 2j * k * np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) wfs_2d_plane = _wfs_plane def wfs_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None, omalias=None): """Plane wave by 2.5-dimensional WFS. :: ____________ D_2.5D(x0,w) = \|j k |xref-x0| n n0 e^(-j k n x0) """ x0 = np.asarray(x0) n0 = np.asarray(n0) n = np.squeeze(np.asarray(n)) xref = np.squeeze(np.asarray(xref)) k = util.wavenumber(omega, c) return wfs_25d_preeq(omega, omalias, c) * \ np.sqrt(2*np.pi * np.linalg.norm(xref - x0)) * \ np.inner(n, n0) * np.exp(-1j * k * np.inner(n, x0)) wfs_3d_plane = _wfs_plane def wfs_25d_preeq(omega, omalias, c): """Preqeualization for 2.5D WFS.""" if omalias is None: return np.sqrt(1j * util.wavenumber(omega, c)) else: if omega <= omalias: return np.sqrt(1j * util.wavenumber(omega, c)) else: return np.sqrt(1j * util.wavenumber(omalias, c)) def delay_3d_plane(omega, x0, n0, n=[0, 1, 0], c=None): """Plane wave by simple delay of secondary sources.""" x0 = np.asarray(x0) n = np.squeeze(np.asarray(n)) k = util.wavenumber(omega, c) return np.exp(-1j * k * np.inner(n, x0)) def source_selection_plane(n0, n): """Secondary source selection for a plane wave. Eq.(13) from [Spors et al, 2008] """ n0 = np.asarray(n0) n = np.squeeze(np.asarray(n)) return np.inner(n, n0) >= defs.selection_tolerance def source_selection_point(n0, x0, xs): """Secondary source selection for a point source. Eq.(15) from [Spors et al, 2008] """ n0 = np.asarray(n0) x0 = np.asarray(x0) xs = np.squeeze(np.asarray(xs)) ds = x0 - xs return inner1d(ds, n0) >= defs.selection_tolerance def source_selection_all(N): """Select all secondary sources.""" return np.ones(N) >= 0 def nfchoa_2d_plane(omega, x0, r0, n=[0, 1, 0], c=None): """Point source by 2.5-dimensional WFS.""" x0 = np.asarray(x0) k = util.wavenumber(omega, c) alpha, beta, r = util.cart2sph(n[0], n[1], n[2]) alpha0, beta0, tmp = util.cart2sph(x0[:, 0], x0[:, 1], x0[:, 2]) # determine max order of circular harmonics M = _hoa_order_2d(len(x0)) # compute driving function d = 0 for m in np.arange(-M, M): d = d + 1j**(-m) / hankel2(m, k * r0) * \ np.exp(1j * m * (alpha0 - alpha)) return - 2j / (np.pi*r0) * d def nfchoa_25d_point(omega, x0, r0, xs, c=None): """Point source by 2.5-dimensional WFS. :: __ (2) 1 \ h|m| (w/c r) D(phi0,w) = ----- /__ ------------- e^(i m (phi0-phi)) 2pi r0 m=-N..N (2) h|m| (w/c r0) """ x0 = np.asarray(x0) k = util.wavenumber(omega, c) alpha, beta, r = util.cart2sph(xs[0], xs[1], xs[2]) alpha0, beta0, tmp = util.cart2sph(x0[:, 0], x0[:, 1], x0[:, 2]) # determine max order of circular harmonics M = _hoa_order_2d(len(x0)) # compute driving function d = 0 a = _sph_hn2(M, k * r) / _sph_hn2(M, k * r0) for m in np.arange(-M, M): d += a[0, abs(m)] * np.exp(1j * m * (alpha0 - alpha)) return 1 / (2 * np.pi * r0) * d def nfchoa_25d_plane(omega, x0, r0, n=[0, 1, 0], c=None): """Plane wave by 2.5-dimensional WFS. :: __ 2i \ i^|m| D_25D(phi0,w) = -- /__ ------------------ e^(i m (phi0-phi_pw) ) r0 m=-N..N (2) w/c h|m| (w/c r0) """ x0 = np.asarray(x0) k = util.wavenumber(omega, c) alpha, beta, r = util.cart2sph(n[0], n[1], n[2]) alpha0, beta0, tmp = util.cart2sph(x0[:, 0], x0[:, 1], x0[:, 2]) # determine max order of circular harmonics M = _hoa_order_2d(len(x0)) # compute driving function d = 0 a = 1 / _sph_hn2(M, k * r0) for m in np.arange(-M, M): d += (1j)**(-abs(m)) * a[0, abs(m)] * \ np.exp(1j * m * (alpha0 - alpha)) return -2 / r0 * d def sdm_2d_line(omega, x0, n0, xs, c=None): """Line source by two-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). Derived from [Spors 2009, 126th AES Convention], Eq.(9), Eq.(4):: D(x0,k) = """ x0 = np.asarray(x0) n0 = np.asarray(n0) xs = np.squeeze(np.asarray(xs)) k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) return - 1j/2 * k * xs[1] / r * hankel2(1, k * r) def sdm_2d_plane(omega, x0, n0, n=[0, 1, 0], c=None): """Plane wave by two-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). Derived from [Ahrens 2011, Springer], Eq.(3.73), Eq.(C.5), Eq.(C.11):: D(x0,k) = kpw,y * e^(-j*kpw,x*x) """ x0 = np.asarray(x0) n0 = np.asarray(n0) n = np.squeeze(np.asarray(n)) k = util.wavenumber(omega, c) return k * n[1] * np.exp(-1j * k * n[0] * x0[:, 0]) def sdm_25d_plane(omega, x0, n0, n=[0, 1, 0], xref=[0, 0, 0], c=None): """Plane wave by 2.5-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). Eq.(3.79) from [Ahrens 2011, Springer]:: D_2.5D(x0,w) = """ x0 = np.asarray(x0) n0 = np.asarray(n0) n = np.squeeze(np.asarray(n)) xref = np.squeeze(np.asarray(xref)) k = util.wavenumber(omega, c) return 4j * np.exp(-1j*k*n[1]*xref[1]) / hankel2(0, k*n[1]*xref[1]) * \ np.exp(-1j*k*n[0]*x0[:, 0]) def sdm_25d_point(omega, x0, n0, xs, xref=[0, 0, 0], c=None): """Point source by 2.5-dimensional SDM. The secondary sources have to be located on the x-axis (y0=0). Driving funcnction from [Spors 2010, 128th AES Covention], Eq.(24):: D(x0,k) = """ x0 = np.asarray(x0) n0 = np.asarray(n0) xs = np.squeeze(np.asarray(xs)) xref = np.squeeze(np.asarray(xref)) k = util.wavenumber(omega, c) ds = x0 - xs r = np.linalg.norm(ds, axis=1) return 1/2 * 1j * k * np.sqrt(xref[1] / (xref[1] - xs[1])) * \ xs[1] / r * hankel2(1, k * r) def _sph_hn2(n, z): """Spherical Hankel function of 2nd kind.""" return np.asarray(sph_jn(n, z)) - 1j * np.asarray(sph_yn(n, z)) def _hoa_order_2d(N): """Computes order of HOA.""" if N % 2 == 0: return N//2 else: return (N-1)//2 PKtEGƅ[sfs/mono/soundfigure.py"""Compute driving functions for sound figures.""" import numpy as np from .. import util from . import drivingfunction def wfs_3d_pw(omega, x0, n0, figure, npw=[0, 0, 1], c=None): """Compute driving function for a 2D sound figure. Based on [Helwani et al., The Synthesis of Sound Figures, MSSP, 2013] """ x0 = np.asarray(x0) n0 = np.asarray(n0) k = util.wavenumber(omega, c) nx, ny = figure.shape # 2D spatial DFT of image figure = np.fft.fftshift(figure, axes=(0, 1)) # sign of spatial DFT figure = np.fft.fft2(figure) # wavenumbers kx = np.fft.fftfreq(nx, 1./nx) ky = np.fft.fftfreq(ny, 1./ny) # shift spectrum due to desired plane wave figure = np.roll(figure, int(k*npw[0]), axis=0) figure = np.roll(figure, int(k*npw[1]), axis=1) # search and iterate over propagating plane wave components kxx, kyy = np.meshgrid(kx, ky, sparse=True) rho = np.sqrt((kxx) ** 2 + (kyy) ** 2) d = 0 for n in range(nx): for m in range(ny): if(rho[n, m] < k): # dispertion relation kz = np.sqrt(k**2 - rho[n, m]**2) # normal vector of plane wave npw = 1/k * np.asarray([kx[n], ky[m], kz]) npw = npw / np.linalg.norm(npw) # driving function of plane wave with positive kz a = drivingfunction.source_selection_plane(n0, npw) a = a * figure[n, m] d += a * drivingfunction.wfs_3d_plane(omega, x0, n0, npw, c) return d PKtEG ykRRsfs/mono/synthesized.py"""Computation of synthesized sound fields.""" import numpy as np from .source import point def generic(omega, x0, n0, d, grid, c=None, source=point): """Compute sound field for a generic driving function.""" d = np.squeeze(np.asarray(d)) if len(d) != len(x0): raise ValueError("length mismatch") p = 0 for weight, position, direction in zip(d, x0, n0): if weight != 0: p += weight * source(omega, position, direction, grid, c) return p def shiftphase(p, phase): """Shift pahse of a sound field.""" return p * np.exp(1j * phase) PKtEGsfs/mono/source.py"""Compute the sound field generated by a sound source. .. plot:: :context: reset import sfs import numpy as np import matplotlib.pyplot as plt plt.rcParams['figure.figsize'] = 8, 4 # inch x0 = 1.5, 1, 0 f = 500 # Hz omega = 2 * np.pi * f grid = sfs.util.xyz_grid([-2, 3], [-1, 2], 0, spacing=0.02) """ import itertools import numpy as np from scipy import special from .. import util def point(omega, x0, n0, grid, c=None): """Point source. :: 1 e^(-j w/c |x-x0|) G(x-x0, w) = --- ----------------- 4pi |x-x0| Examples -------- .. plot:: :context: close-figs p_point = sfs.mono.source.point(omega, x0, None, grid) sfs.plot.soundfield(p_point, grid) plt.title("Point Source at {} m".format(x0)) Normalization ... multiply by :math:`4\pi` ... .. plot:: :context: close-figs p_point *= 4 * np.pi sfs.plot.soundfield(p_point, grid) plt.title("Point Source at {} m (normalized)".format(x0)) """ k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) grid = util.asarray_of_arrays(grid) r = np.linalg.norm(grid - x0) return np.squeeze(1/(4*np.pi) * np.exp(-1j * k * r) / r) def point_modal(omega, x0, n0, grid, L, N=None, deltan=0, c=None): """Point source in a rectangular room using a modal room model. Parameters ---------- omega : float Frequency of source. x0 : (3,) array_like Position of source. n0 : (3,) array_like Normal vector (direction) of source (only required for compatibility). grid : triple of numpy.ndarray The grid that is used for the sound field calculations. L : (3,) array_like Dimensionons of the rectangular room. N : (3,) array_like or int, optional Combination of modal orders in the three-spatial dimensions to calculate the sound field for or maximum order for all dimensions. If not given, the maximum modal order is approximately determined and the sound field is computed up to this maximum order. deltan : float, optional Absorption coefficient of the walls. c : float, optional Speed of sound. Returns ------- numpy.ndarray Sound pressure at positions given by `grid`. """ k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) x, y, z = util.asarray_of_arrays(grid) if N is None: # determine maximum modal order per dimension Nx = int(np.ceil(L[0]/np.pi * k)) Ny = int(np.ceil(L[1]/np.pi * k)) Nz = int(np.ceil(L[2]/np.pi * k)) mm = range(Nx) nn = range(Ny) ll = range(Nz) elif np.isscalar(N): # compute up to a given order mm = range(N) nn = range(N) ll = range(N) else: # compute field for one order combination only mm = [N[0]] nn = [N[1]] ll = [N[2]] kmp0 = [((kx + 1j * deltan)**2, np.cos(kx * x) * np.cos(kx * x0[0])) for kx in [m * np.pi / L[0] for m in mm]] kmp1 = [((ky + 1j * deltan)**2, np.cos(ky * y) * np.cos(ky * x0[1])) for ky in [n * np.pi / L[1] for n in nn]] kmp2 = [((kz + 1j * deltan)**2, np.cos(kz * z) * np.cos(kz * x0[2])) for kz in [l * np.pi / L[2] for l in ll]] ksquared = k**2 p = 0 for (km0, p0), (km1, p1), (km2, p2) in itertools.product(kmp0, kmp1, kmp2): km = km0 + km1 + km2 p = p + 1 / (ksquared - km) * p0 * p1 * p2 return np.squeeze(p) def line(omega, x0, n0, grid, c=None): """Line source parallel to the z-axis. Note: third component of x0 is ignored. :: (2) G(x-x0, w) = -j/4 H0 (w/c |x-x0|) Examples -------- .. plot:: :context: close-figs p_line = sfs.mono.source.line(omega, x0, None, grid) sfs.plot.soundfield(p_line, grid) plt.title("Line Source at {} m".format(x0[:2])) Normalization ... .. plot:: :context: close-figs p_line *= np.exp(-1j*7*np.pi/4) / np.sqrt(1/(8*np.pi*omega/sfs.defs.c)) sfs.plot.soundfield(p_line, grid) plt.title("Line Source at {} m (normalized)".format(x0[:2])) """ k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) x0 = x0[:2] # ignore z-component grid = util.asarray_of_arrays(grid) r = np.linalg.norm(grid[:2] - x0) p = -1j/4 * special.hankel2(0, k * r) p = _duplicate_zdirection(p, grid) return np.squeeze(p) def line_dipole(omega, x0, n0, grid, c=None): """Line source with dipole characteristics parallel to the z-axis. Note: third component of x0 is ignored. :: (2) G(x-x0, w) = jk/4 H1 (w/c |x-x0|) cos(phi) """ k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) x0 = x0[:2] # ignore z-component n0 = n0[:2] grid = util.asarray_of_arrays(grid) dx = grid[:2] - x0 r = np.linalg.norm(dx) p = 1j*k/4 * special.hankel2(1, k * r) * np.inner(dx, n0) / r p = _duplicate_zdirection(p, grid) return np.squeeze(p) def plane(omega, x0, n0, grid, c=None): """Plane wave. :: G(x, w) = e^(-i w/c n x) Example ------- .. plot:: :context: close-figs direction = 45 # degree n0 = sfs.util.direction_vector(np.radians(direction)) p_plane = sfs.mono.source.plane(omega, x0, n0, grid) sfs.plot.soundfield(p_plane, grid); plt.title("Plane wave with direction {} degree".format(direction)) """ k = util.wavenumber(omega, c) x0 = util.asarray_1d(x0) n0 = util.asarray_1d(n0) grid = util.asarray_of_arrays(grid) return np.squeeze(np.exp(-1j * k * np.inner(grid - x0, n0))) def _duplicate_zdirection(p, grid): """If necessary, duplicate field in z-direction.""" gridshape = np.broadcast(*grid).shape if len(gridshape) > 2: return np.tile(p, [1, 1, gridshape[2]]) else: return p PKpHGH&&#sfs-0.1.1.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 PKpHGX!sfs-0.1.1.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.1"}PKpHGפS!sfs-0.1.1.dist-info/top_level.txtsfs PKpHG3onnsfs-0.1.1.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.24.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PKpHG=5 5 sfs-0.1.1.dist-info/METADATAMetadata-Version: 2.0 Name: sfs Version: 0.1.1 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 PKpHG\$RRsfs-0.1.1.dist-info/RECORDsfs-0.1.1.dist-info/DESCRIPTION.rst,sha256=hPOr7309iWWvJCttPmjfbrJKxoiwOz8TpRkSUb4OBgI,2086 sfs-0.1.1.dist-info/WHEEL,sha256=AvR0WeTpDaxT645bl5FQxUK6NPsTls2ttpcGJg3j1Xg,110 sfs-0.1.1.dist-info/top_level.txt,sha256=6mBnHRnczkH-3MoFY3vtAo_cer661XzjlrnrFOWAW28,4 sfs-0.1.1.dist-info/metadata.json,sha256=n_p2f-_XmuqD2kvEfOdSwgt-Vmhn8UoOA1l4k9-gdq4,793 sfs-0.1.1.dist-info/RECORD,, sfs-0.1.1.dist-info/METADATA,sha256=_RkjzvVw4lGVun_fh8t8Gt7k6Q6CRoNCliejcb7GUos,2613 sfs/_version.py,sha256=hMTo5JBt34-XEpZSzyQEFlOvZ9TA8wGxmas2jpmw7qg,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 sfs/mono/__init__.py,sha256=41iYheUFsZdwqZCrsd7E195o6418XugcNidySA3RVDA,103 sfs/mono/drivingfunction.py,sha256=6meIkE5TnlKPHGUVUtxiamNsN_RIKWMMlBdDyK600Vo,8717 sfs/mono/soundfigure.py,sha256=JgkmzFYOwAF99Jx6_dwN0WWpd1RN47g_0ko-VG1mrIY,1563 sfs/mono/synthesized.py,sha256=Jp8UxsxuwnGxAvm0BuRDEsrYWp1HmGHKoYJHpCGaiwQ,594 sfs/mono/source.py,sha256=pu50VZq8UBHdNFWAZfaggTsy12BRQzH8rGss3NxSvMs,6126 PKdpHG'PPsfs/_version.pyPKrqE!Zs}sfs/__init__.pyPK2dHG+A5A5 sfs/array.pyPK2dHG+ 7sfs/util.pyPKtEGK Psfs/plot.pyPKtEG]y jsfs/defs.pyPKhEVrksfs/tapering.pyPKU%FVWMggwrsfs/mono/__init__.pyPKtEG=ݺ " "ssfs/mono/drivingfunction.pyPKtEGƅ[Vsfs/mono/soundfigure.pyPKtEG ykRRsfs/mono/synthesized.pyPKtEG-sfs/mono/source.pyPKpHGH&&#Ksfs-0.1.1.dist-info/DESCRIPTION.rstPKpHGX!sfs-0.1.1.dist-info/metadata.jsonPKpHGפS! sfs-0.1.1.dist-info/top_level.txtPKpHG3onnMsfs-0.1.1.dist-info/WHEELPKpHG=5 5 sfs-0.1.1.dist-info/METADATAPKpHG\$RRasfs-0.1.1.dist-info/RECORDPK