Sound Field Synthesis (SFS) Toolbox for Python

A Python library for creating numercial simulations of sound field synthesis methods like Wave Field Synthesis (WFS) or Near-Field Compensated Higher Order Ambisonics (NFC-HOA).

Documentation:

https://sfs-python.readthedocs.io/

Source code and issue tracker:

https://github.com/sfstoolbox/sfs-python/

License:

MIT – see the file LICENSE for details.

Quick start:
  • Install Python 3, NumPy, SciPy and Matplotlib

  • python3 -m pip install sfs --user

  • Check out the examples in the documentation

More information about the underlying theory can be found at https://sfs.readthedocs.io/. There is also a Sound Field Synthesis Toolbox for Octave/Matlab, see https://sfs-matlab.readthedocs.io/.


Installation

Requirements

Obviously, you’ll need Python. More specifically, you’ll need Python 3. NumPy and SciPy are needed for the calculations. If you want to use the provided functions for plotting sound fields, you’ll need Matplotlib. However, since all results are provided as plain NumPy arrays, you should also be able to use any plotting library of your choice to visualize the sound fields.

Instead of installing all of the requirements separately, you should probably get a Python distribution that already includes everything, e.g. Anaconda.

Installation

Once you have installed the above-mentioned dependencies, you can use pip to download and install the latest release of the Sound Field Synthesis Toolbox with a single command:

python3 -m 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:

python3 -m pip uninstall sfs

If you want to install the latest development version of the SFS Toolbox, have a look at Contributing.

Examples

You can play with the Jupyter notebooks (without having to install anything) by clicking binder logo on the respective example page.

Sound Field Synthesis

Illustrates the usage of the SFS toolbox for the simulation of different sound field synthesis methods.

[1]:
import numpy as np
import matplotlib.pyplot as plt
import sfs
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/conda/0.6.2/lib/python3.9/site-packages/traitlets/traitlets.py:3030: FutureWarning: --rc={'figure.dpi': 96} for dict-traits is deprecated in traitlets 5.0. You can pass --rc <key=value> ... multiple times to add items to a dict.
  warn(
[2]:
# Simulation parameters
number_of_secondary_sources = 56
frequency = 680  # in Hz
pw_angle = 30  # traveling direction of plane wave in degree
xs = [-2, -1, 0]  # position of virtual point source in m

grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)
omega = 2 * np.pi * frequency  # angular frequency
npw = sfs.util.direction_vector(np.radians(pw_angle))  # normal vector of plane wave

Define a helper function for synthesize and plot the sound field from the given driving signals.

[3]:
def sound_field(d, selection, secondary_source, array, grid, tapering=True):
    if tapering:
        tapering_window = sfs.tapering.tukey(selection, alpha=0.3)
    else:
        tapering_window = sfs.tapering.none(selection)
    p = sfs.fd.synthesize(d, tapering_window, array, secondary_source, grid=grid)
    sfs.plot2d.amplitude(p, grid, xnorm=[0, 0, 0])
    sfs.plot2d.loudspeakers(array.x, array.n, tapering_window)

Circular loudspeaker arrays

In the following we show different sound field synthesis methods applied to a circular loudspeaker array.

[4]:
radius = 1.5  # in m
array = sfs.array.circular(number_of_secondary_sources, radius)
Wave Field Synthesis (WFS)
Plane wave
[5]:
d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, n=npw)
sound_field(d, selection, secondary_source, array, grid)
_images/examples_sound-field-synthesis_8_0.svg
Point source
[6]:
d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs)
sound_field(d, selection, secondary_source, array, grid)
_images/examples_sound-field-synthesis_10_0.svg
Near-Field Compensated Higher Order Ambisonics (NFC-HOA)
Plane wave
[7]:
d, selection, secondary_source = sfs.fd.nfchoa.plane_25d(omega, array.x, radius, n=npw)
sound_field(d, selection, secondary_source, array, grid, tapering=False)
_images/examples_sound-field-synthesis_12_0.svg
Point source
[8]:
d, selection, secondary_source = sfs.fd.nfchoa.point_25d(omega, array.x, radius, xs)
sound_field(d, selection, secondary_source, array, grid, tapering=False)
_images/examples_sound-field-synthesis_14_0.svg

Linear loudspeaker array

In the following we show different sound field synthesis methods applied to a linear loudspeaker array.

[9]:
spacing = 0.07  # in m
array = sfs.array.linear(number_of_secondary_sources, spacing,
                         center=[0, -0.5, 0], orientation=[0, 1, 0])
Wave Field Synthesis (WFS)
Plane wave
[10]:
d, selection, secondary_source = sfs.fd.wfs.plane_25d(omega, array.x, array.n, npw)
sound_field(d, selection, secondary_source, array, grid)
_images/examples_sound-field-synthesis_18_0.svg
Point source
[11]:
d, selection, secondary_source = sfs.fd.wfs.point_25d(omega, array.x, array.n, xs)
sound_field(d, selection, secondary_source, array, grid)
_images/examples_sound-field-synthesis_20_0.svg

Mirror Image Sources and the Sound Field in a Rectangular Room

[1]:
import matplotlib.pyplot as plt
import numpy as np
import sfs
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/conda/0.6.2/lib/python3.9/site-packages/traitlets/traitlets.py:3030: FutureWarning: --rc={'figure.dpi': 96} for dict-traits is deprecated in traitlets 5.0. You can pass --rc <key=value> ... multiple times to add items to a dict.
  warn(
[2]:
L = 2, 2.7, 3  # room dimensions
x0 = 1.2, 1.7, 1.5  # source position
max_order = 2  # maximum order of image sources
coeffs = .8, .8, .6, .6, .7, .7  # wall reflection coefficients

2D Mirror Image Sources

[3]:
xs, wall_count = sfs.util.image_sources_for_box(x0[0:2], L[0:2], max_order)
source_strength = np.prod(coeffs[0:4]**wall_count, axis=1)
[4]:
from matplotlib.patches import Rectangle
[5]:
fig, ax = plt.subplots()
ax.scatter(*xs.T, source_strength * 20)
ax.add_patch(Rectangle((0, 0), L[0], L[1], fill=False))
ax.set_xlabel('x / m')
ax.set_ylabel('y / m')
ax.axis('equal');
[5]:
(-3.1999999999999997, 5.6000000000000005, -4.24, 7.640000000000001)
_images/examples_mirror-image-source-model_6_1.svg

Monochromatic Sound Field

[6]:
omega = 2 * np.pi * 1000  # angular frequency
[7]:
grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.02)
P = sfs.fd.source.point_image_sources(omega, x0, grid, L,
                                      max_order=max_order, coeffs=coeffs)
[8]:
sfs.plot2d.amplitude(P, grid, xnorm=[L[0]/2, L[1]/2, L[2]/2]);
[8]:
<matplotlib.image.AxesImage at 0x7f6ac8adb190>
_images/examples_mirror-image-source-model_10_1.svg

Spatio-temporal Impulse Response

[9]:
fs = 44100  # sample rate
signal = [1, 0, 0], fs
[10]:
grid = sfs.util.xyz_grid([0, L[0]], [0, L[1]], 1.5, spacing=0.005)
p = sfs.td.source.point_image_sources(x0, signal, 0.004, grid, L, max_order,
                                      coeffs=coeffs)
[11]:
sfs.plot2d.level(p, grid)
sfs.plot2d.virtualsource(x0)
_images/examples_mirror-image-source-model_14_0.svg

Animations of a Pulsating Sphere

[1]:
import sfs
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import HTML
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/conda/0.6.2/lib/python3.9/site-packages/traitlets/traitlets.py:3030: FutureWarning: --rc={'figure.dpi': 96} for dict-traits is deprecated in traitlets 5.0. You can pass --rc <key=value> ... multiple times to add items to a dict.
  warn(

In this example, the sound field of a pulsating sphere is visualized. Different acoustic variables, such as sound pressure, particle velocity, and particle displacement, are simulated. The first two quantities are computed with

while the last one can be obtained by using

which converts the particle velocity into displacement.

A couple of additional functions are implemented in

in order to help creating animating pictures, which is fun!

[2]:
import animations_pulsating_sphere as animation
[3]:
# Pulsating sphere
center = [0, 0, 0]
radius = 0.25
amplitude = 0.05
f = 1000  # frequency
omega = 2 * np.pi * f  # angular frequency

# Axis limits
figsize = (6, 6)
xmin, xmax = -1, 1
ymin, ymax = -1, 1

# Animations
frames = 20  # frames per period

Particle Displacement

[4]:
grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.025)
ani = animation.particle_displacement(
        omega, center, radius, amplitude, grid, frames, figsize, c='Gray')
plt.close()
HTML(ani.to_jshtml())
[4]:

Click the arrow button to start the animation. to_jshtml() allows you to play with the animation, e.g. speed up/down the animation (+/- button). Try to reverse the playback by clicking the left arrow. You’ll see a sound sink.

You can also show the animation by using to_html5_video(). See the documentation for more detail.

Of course, different types of grid can be chosen. Below is the particle animation using the same parameters but with a hexagonal grid.

[5]:
def hex_grid(xlim, ylim, hex_edge, align='horizontal'):
    if align is 'vertical':
        umin, umax = ylim
        vmin, vmax = xlim
    else:
        umin, umax = xlim
        vmin, vmax = ylim
    du = np.sqrt(3) * hex_edge
    dv = 1.5 * hex_edge
    num_u = int((umax - umin) / du)
    num_v = int((vmax - vmin) / dv)
    u, v = np.meshgrid(np.linspace(umin, umax, num_u),
                       np.linspace(vmin, vmax, num_v))
    u[::2] += 0.5 * du

    if align is 'vertical':
        grid = v, u, 0
    elif align is 'horizontal':
        grid = u, v, 0
    return  grid
<>:2: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:16: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:18: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:2: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:16: SyntaxWarning: "is" with a literal. Did you mean "=="?
<>:18: SyntaxWarning: "is" with a literal. Did you mean "=="?
<ipython-input-1-17990c1f260e>:2: SyntaxWarning: "is" with a literal. Did you mean "=="?
  if align is 'vertical':
<ipython-input-1-17990c1f260e>:16: SyntaxWarning: "is" with a literal. Did you mean "=="?
  if align is 'vertical':
<ipython-input-1-17990c1f260e>:18: SyntaxWarning: "is" with a literal. Did you mean "=="?
  elif align is 'horizontal':
[6]:
grid = hex_grid([xmin, xmax], [ymin, ymax], 0.0125, 'vertical')
ani = animation.particle_displacement(
        omega, center, radius, amplitude, grid, frames, figsize, c='Gray')
plt.close()
HTML(ani.to_jshtml())
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/checkouts/0.6.2/doc/examples/animations_pulsating_sphere.py:18: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  scat = sfs.plot2d.particles(grid + displacement, **kwargs)
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/checkouts/0.6.2/doc/examples/animations_pulsating_sphere.py:21: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  position = (grid + displacement * phasor**i).apply(np.real)
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/checkouts/0.6.2/doc/examples/animations_pulsating_sphere.py:21: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  position = (grid + displacement * phasor**i).apply(np.real)
[6]:

Another one using a random grid.

[7]:
grid = [np.random.uniform(xmin, xmax, 4000),
        np.random.uniform(ymin, ymax, 4000), 0]
ani = animation.particle_displacement(
        omega, center, radius, amplitude, grid, frames, figsize, c='Gray')
plt.close()
HTML(ani.to_jshtml())
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/checkouts/0.6.2/doc/examples/animations_pulsating_sphere.py:18: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  scat = sfs.plot2d.particles(grid + displacement, **kwargs)
/home/docs/checkouts/readthedocs.org/user_builds/sfs-python/checkouts/0.6.2/doc/examples/animations_pulsating_sphere.py:21: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray.
  position = (grid + displacement * phasor**i).apply(np.real)
[7]:

Each grid has its strengths and weaknesses. Please refer to the on-line discussion.

Particle Velocity

[8]:
amplitude = 1e-3
grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.04)
ani = animation.particle_velocity(
        omega, center, radius, amplitude, grid, frames, figsize)
plt.close()
HTML(ani.to_jshtml())
[8]:

Please notice that the amplitude of the pulsating motion is adjusted so that the arrows are neither too short nor too long. This kind of compromise is inevitable since

\[\text{(particle velocity)} = \text{i} \omega \times (\text{amplitude}),\]

thus the absolute value of particle velocity is usually much larger than that of amplitude. It should be also kept in mind that the hole in the middle does not visualizes the exact motion of the pulsating sphere. According to the above equation, the actual amplitude should be much smaller than the arrow lengths. The changing rate of its size is also two times higher than the original frequency.

Sound Pressure

[9]:
amplitude = 0.05
impedance_pw = sfs.default.rho0 * sfs.default.c
max_pressure = omega * impedance_pw * amplitude

grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)
ani = animation.sound_pressure(
        omega, center, radius, amplitude, grid, frames, pulsate=True,
        figsize=figsize, vmin=-max_pressure, vmax=max_pressure)
plt.close()
HTML(ani.to_jshtml())
[9]:

Notice that the sound pressure exceeds the atmospheric pressure (\(\approx 10^5\) Pa), which of course makes no sense. This is due to the large amplitude (50 mm) of the pulsating motion. It was chosen to better visualize the particle movements in the earlier animations.

For 1 kHz, the amplitude corresponding to a moderate sound pressure, let say 1 Pa, is in the order of micrometer. As it is very small compared to the corresponding wavelength (0.343 m), the movement of the particles and the spatial structure of the sound field cannot be observed simultaneously. Furthermore, at high frequencies, the sound pressure for a given particle displacement scales with the frequency. The smaller wavelength (higher frequency) we choose, it is more likely to end up with a prohibitively high sound pressure.

In the following examples, the amplitude is set to a realistic value 1 \(\mu\)m. Notice that the pulsating motion of the sphere is no more visible.

[10]:
amplitude = 1e-6
impedance_pw = sfs.default.rho0 * sfs.default.c
max_pressure = omega * impedance_pw * amplitude

grid = sfs.util.xyz_grid([xmin, xmax], [ymin, ymax], 0, spacing=0.005)
ani = animation.sound_pressure(
        omega, center, radius, amplitude, grid, frames, pulsate=True,
        figsize=figsize, vmin=-max_pressure, vmax=max_pressure)
plt.close()
HTML(ani.to_jshtml())
[10]:

Let’s zoom in closer to the boundary of the sphere.

[11]:
L = 10 * amplitude
xmin_zoom, xmax_zoom = radius - L, radius + L
ymin_zoom, ymax_zoom = -L, L
[12]:
grid = sfs.util.xyz_grid([xmin_zoom, xmax_zoom], [ymin_zoom, ymax_zoom], 0, spacing=L / 100)
ani = animation.sound_pressure(
        omega, center, radius, amplitude, grid, frames, pulsate=True,
        figsize=figsize, vmin=-max_pressure, vmax=max_pressure)
plt.close()
HTML(ani.to_jshtml())
[12]:

This shows how the vibrating motion of the sphere (left half) changes the sound pressure of the surrounding air (right half). Notice that the sound pressure increases/decreases (more red/blue) when the surface accelerates/decelerates.

Example Python Scripts

Various example scripts are located in the directory doc/examples/, e.g.

API Documentation

Sound Field Synthesis Toolbox.

https://sfs-python.readthedocs.io/

Submodules

fd

Submodules for monochromatic sound fields.

td

Submodules for broadband sound fields.

array

Compute positions of various secondary source distributions.

tapering

Weights (tapering) for the driving function.

plot2d

2D plots of sound fields etc.

plot3d

3D plots of sound fields etc.

util

Various utility functions.

class sfs.default[source]

Get/set defaults for the sfs module.

For example, when you want to change the default speed of sound:

import sfs
sfs.default.c = 330
c = 343

Speed of sound.

rho0 = 1.225

Static density of air.

selection_tolerance = 1e-06

Tolerance used for secondary source selection.

reset()[source]

Reset all attributes to their “factory default”.

References

Ahr12

J. Ahrens. Analytic Methods of Sound Field Synthesis. Springer, Berlin Heidelberg, 2012. doi:10.1007/978-3-642-25743-8.

AB79

J. B. Allen and D. A. Berkley. Image method for efficiently simulating small-room acoustics. Journal of the Acoustical Society of America, 65:943–950, 1979. doi:10.1121/1.382599.

Bor84

J. Borish. Extension of the image model to arbitrary polyhedra. Journal of the Acoustical Society of America, 75:1827–1836, 1984. doi:10.1121/1.390983.

FFSS17

Gergely Firtha, Péter Fiala, Frank Schultz, and Sascha Spors. Improved Referencing Schemes for 2.5D Wave Field Synthesis Driving Functions. IEEE/ACM Trans. Audio Speech Language Process., 25(5):1117–1127, 2017. doi:10.1109/TASLP.2017.2689245.

Mos12

M. Möser. Technische Akustik. Springer, Berlin Heidelberg, 2012. doi:10.1007/978-3-642-30933-5.

Sch16

Frank Schultz. Sound Field Synthesis for Line Source Array Applications in Large-Scale Sound Reinforcement. PhD thesis, University of Rostock, 2016. doi:10.18453/rosdok_id00001765.

SA09

S. Spors and J. Ahrens. Spatial Sampling Artifacts of Wave Field Synthesis for the Reproduction of Virtual Point Sources. In 126th Convention of the Audio Engineering Society. 2009. URL: http://bit.ly/2jkfboi.

SA10

S. Spors and J. Ahrens. Analysis and Improvement of Pre-equalization in 2.5-dimensional Wave Field Synthesis. In 128th Convention of the Audio Engineering Society. 2010. URL: http://bit.ly/2Ad6RRR.

SRA08

S. Spors, R. Rabenstein, and J. Ahrens. The Theory of Wave Field Synthesis Revisited. In 124th Convention of the Audio Engineering Society. 2008. URL: http://bit.ly/2ByRjnB.

SSR16

S. Spors, F. Schultz, and T. Rettberg. Improved Driving Functions for Rectangular Loudspeaker Arrays Driven by Sound Field Synthesis. In 42nd German Annual Conference on Acoustics (DAGA). 2016. URL: http://bit.ly/2AWRo7G.

Sta97

Evert W. Start. Direct Sound Enhancement by Wave Field Synthesis. PhD thesis, Delft University of Technology, 1997.

Wie14

H. Wierstorf. Perceptual Assessment of Sound Field Synthesis. PhD thesis, Technische Universität Berlin, 2014. doi:10.14279/depositonce-4310.

Contributing

If you find errors, omissions, inconsistencies or other things that need improvement, please create an issue or a pull request at https://github.com/sfstoolbox/sfs-python/. Contributions are always welcome!

Development Installation

Instead of pip-installing the latest release from PyPI, you should get the newest development version from Github:

git clone https://github.com/sfstoolbox/sfs-python.git
cd sfs-python
python3 -m pip install --user -e .

… where -e stands for --editable.

This way, your installation always stays up-to-date, even if you pull new changes from the Github repository.

Building the Documentation

If you make changes to the documentation, you can re-create the HTML pages using Sphinx. You can install it and a few other necessary packages with:

python3 -m pip install -r doc/requirements.txt --user

To create the HTML pages, use:

python3 setup.py build_sphinx

The generated files will be available in the directory build/sphinx/html/.

To create the EPUB file, use:

python3 setup.py build_sphinx -b epub

The generated EPUB file will be available in the directory build/sphinx/epub/.

To create the PDF file, use:

python3 setup.py build_sphinx -b latex

Afterwards go to the folder build/sphinx/latex/ and run LaTeX to create the PDF file. If you don’t know how to create a PDF file from the LaTeX output, you should have a look at Latexmk (see also this Latexmk tutorial).

It is also possible to automatically check if all links are still valid:

python3 setup.py build_sphinx -b linkcheck

Running the Tests

You’ll need pytest for that. It can be installed with:

python3 -m pip install -r tests/requirements.txt --user

To execute the tests, simply run:

python3 -m pytest

Creating a New Release

New releases are made using the following steps:

  1. Bump version number in sfs/__init__.py

  2. Update NEWS.rst

  3. Commit those changes as “Release x.y.z”

  4. Create an (annotated) tag with git tag -a x.y.z

  5. Clear the dist/ directory

  6. Create a source distribution with python3 setup.py sdist

  7. Create a wheel distribution with python3 setup.py bdist_wheel

  8. Check that both files have the correct content

  9. Upload them to PyPI with twine: python3 -m twine upload dist/*

  10. Push the commit and the tag to Github and add release notes containing a link to PyPI and the bullet points from NEWS.rst

  11. Check that the new release was built correctly on RTD and select the new release as default version

Version History

Version 0.6.2 (2021-06-05):
  • build doc fix, use sphinx4, mathjax2, html_css_files

Version 0.6.1 (2021-06-05):
Version 0.6.0 (2020-12-01):
Version 0.5.0 (2019-03-18):
Version 0.4.0 (2018-03-14):
Version 0.3.1 (2016-04-08):
  • Fixed metadata of release

Version 0.3.0 (2016-04-08):
  • Dirichlet Green’s function for the scattering of a line source at an edge

  • Driving functions for the synthesis of various virtual source types with edge-shaped arrays by the equivalent scattering appoach

  • Driving functions for the synthesis of focused sources by WFS

Version 0.2.0 (2015-12-11):
  • Ability to calculate and plot particle velocity and displacement fields

  • Several function name and parameter name changes

Version 0.1.1 (2015-10-08):
  • Fix missing sfs.mono subpackage in PyPI packages

Version 0.1.0 (2015-09-22):

Initial release.