Skip to content

Commit 71415ba

Browse files
committed
2.5D WFS referencing schemes line, circ, point
1 parent da22502 commit 71415ba

File tree

4 files changed

+282
-2
lines changed

4 files changed

+282
-2
lines changed

doc/examples.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Examples
1313
.. nbgallery::
1414

1515
examples/sound-field-synthesis
16+
examples/wfs-referencing
1617
examples/modal-room-acoustics
1718
examples/mirror-image-source-model
1819
examples/animations-pulsating-sphere

doc/examples/wfs-referencing.ipynb

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# 2.5D WFS Referencing Schemes\n",
8+
"\n",
9+
"This notebook illustrates the usage of the SFS toolbox for the simulation of different 2.5D WFS referencing schemes.\n",
10+
"A dedicated referencing scheme allows correct amplitude alongside a reference contour within the listening area.\n",
11+
"For the theory please check\n",
12+
"Ch 3.1-3.3 in <cite data-cite=\"Start1997\">(Start1997)</cite>,\n",
13+
"Ch. 4.1.3 in <cite data-cite=\"Firtha2019\">(Firtha2019)</cite> and\n",
14+
"<cite data-cite=\"Firtha2017\">(Firtha2017)</cite>."
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"import matplotlib.pyplot as plt\n",
24+
"import numpy as np\n",
25+
"import sfs"
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {},
32+
"outputs": [],
33+
"source": [
34+
"# we test the usage of\n",
35+
"# https://github.com/sfstoolbox/sfs-python/pull/194\n",
36+
"def normalize_rows(x):\n",
37+
" \"\"\"Normalize a list of vectors.\"\"\"\n",
38+
" x = sfs.util.asarray_of_rows(x)\n",
39+
" return x / np.linalg.norm(x, axis=1, keepdims=True)\n",
40+
"# to be removed once this is merged"
41+
]
42+
},
43+
{
44+
"cell_type": "markdown",
45+
"metadata": {},
46+
"source": [
47+
"## Circular loudspeaker arrays"
48+
]
49+
},
50+
{
51+
"cell_type": "code",
52+
"execution_count": null,
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"R = 1.5 # radius [m] of circular loudspeaker array\n",
57+
"array = sfs.array.circular(N=64, R=R) # with N loudspeakers\n",
58+
"grid = sfs.util.xyz_grid([-2, 2], [-2, 2], 0, spacing=0.02)\n",
59+
"\n",
60+
"xs = -4, 0, 0 # virtual point source on negative x-axis\n",
61+
"wavelength = 1 / 4 # m"
62+
]
63+
},
64+
{
65+
"cell_type": "code",
66+
"execution_count": null,
67+
"metadata": {},
68+
"outputs": [],
69+
"source": [
70+
"def sound_field(d, selection, array, secondary_source, grid, xref):\n",
71+
" p = sfs.fd.synthesize(d, selection, array, secondary_source, grid=grid)\n",
72+
" fig, [ax_amp, ax_lvl] = plt.subplots(2, 1, sharex=True)\n",
73+
" fig.set_figheight(fig.get_figwidth() * 3/2)\n",
74+
" sfs.plot2d.amplitude(p, grid, vmax=2, vmin=-2, ax=ax_amp)\n",
75+
" sfs.plot2d.level(p, grid, vmax=6, vmin=-6, ax=ax_lvl,\n",
76+
" colorbar_kwargs={'label': 'dB'})\n",
77+
" sfs.plot2d.level_contour(p, grid, levels=[0], colors='w', ax=ax_lvl)\n",
78+
" for ax in ax_amp, ax_lvl:\n",
79+
" sfs.plot2d.loudspeakers(array.x, array.n, selection, size=0.125, ax=ax)\n",
80+
" ax_lvl.scatter(*xref[selection, :2].T, marker='o', s=20, c='lightsalmon',\n",
81+
" zorder=3)\n",
82+
" plt.tight_layout()\n",
83+
" return p"
84+
]
85+
},
86+
{
87+
"cell_type": "code",
88+
"execution_count": null,
89+
"metadata": {},
90+
"outputs": [],
91+
"source": [
92+
"xs = sfs.util.asarray_of_rows(xs)\n",
93+
"frequency = sfs.default.c / wavelength # Hz\n",
94+
"omega = 2 * np.pi * frequency # rad/s\n",
95+
"normalize_gain = 4 * np.pi * np.linalg.norm(xs)"
96+
]
97+
},
98+
{
99+
"cell_type": "markdown",
100+
"metadata": {},
101+
"source": [
102+
"### Line as reference contour\n",
103+
"\n",
104+
"The reference contour is calculated according to eqs. (24), (31), (52) in <cite data-cite=\"Firtha2017\">(Firtha2017)</cite>. \n",
105+
"The code assumes a virtual point source on x-axis.\n",
106+
"The reference contour is a straight line on y-axis."
107+
]
108+
},
109+
{
110+
"cell_type": "code",
111+
"execution_count": null,
112+
"metadata": {},
113+
"outputs": [],
114+
"source": [
115+
"xref_line = 0\n",
116+
"cosbeta = (array.n @ [1, 0, 0]).reshape(-1, 1)\n",
117+
"xref = array.x + \\\n",
118+
" (xs - array.x) * (xref_line + R * cosbeta) / (xs[0, 0] + R * cosbeta)\n",
119+
"\n",
120+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
121+
" omega, array.x, array.n, xs, xref=xref)\n",
122+
"p_line = sound_field(\n",
123+
" d * normalize_gain, selection, array, secondary_source, grid, xref)"
124+
]
125+
},
126+
{
127+
"cell_type": "markdown",
128+
"metadata": {},
129+
"source": [
130+
"The level plot includes a white 0 dB isobar curve.\n",
131+
"The orange-like dots represent the stationary phase points at which amplitude correct synthesis is to be expected.\n",
132+
"These dots shape the line reference contour.\n",
133+
"Note that the isobar curve is not perfectly aligned along line reference contour due to diffraction artifacts."
134+
]
135+
},
136+
{
137+
"cell_type": "markdown",
138+
"metadata": {},
139+
"source": [
140+
"### Circle as reference contour\n",
141+
"\n",
142+
"This reference contour is a circle with its origin at xs and a radius |xs|. This contour is obtained with more straightforward vector calculus than the previous example."
143+
]
144+
},
145+
{
146+
"cell_type": "code",
147+
"execution_count": null,
148+
"metadata": {},
149+
"outputs": [],
150+
"source": [
151+
"# reference contour is a circle with origin xs and radius |xs|\n",
152+
"xref_dist = np.linalg.norm(xs)\n",
153+
"# calc reference contour xref(x0), cf. [Firtha19, eq. (24), (31)]\n",
154+
"xref = xs + xref_dist * normalize_rows(array.x - xs)\n",
155+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
156+
" omega, array.x, array.n, xs, xref=xref)\n",
157+
"p_circ = sound_field(\n",
158+
" d * normalize_gain, selection, array, secondary_source, grid, xref)"
159+
]
160+
},
161+
{
162+
"cell_type": "markdown",
163+
"metadata": {},
164+
"source": [
165+
"### Reference point"
166+
]
167+
},
168+
{
169+
"cell_type": "markdown",
170+
"metadata": {},
171+
"source": [
172+
"The default handling in\n",
173+
"`point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None)`\n",
174+
"uses just a reference point xref, and more specifically this default point is the origin of the coordinate system.\n",
175+
"This single point xref, the virtual source position xs and the loudspeaker array geometry together determine the reference contour without further user access to it.\n",
176+
"This handling is chosen due to convenience and practical relevance when working with circular loudspeaker arrays.\n",
177+
"\n",
178+
"The example below shows the resulting reference contour for the default case.\n",
179+
"In the example it looks similar to the line reference contour, but is in general not exactly the same.\n",
180+
"For example, please try a virtual point source that is far away from the array."
181+
]
182+
},
183+
{
184+
"cell_type": "code",
185+
"execution_count": null,
186+
"metadata": {},
187+
"outputs": [],
188+
"source": [
189+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
190+
" omega, array.x, array.n, xs)\n",
191+
"p_point = sound_field(\n",
192+
" d * normalize_gain, selection, array, secondary_source,\n",
193+
" grid, np.array([[0, 0, 0]]*array.x.shape[0]))"
194+
]
195+
},
196+
{
197+
"cell_type": "markdown",
198+
"metadata": {},
199+
"source": [
200+
"Points with amplitude correct synthesis need to be stationary phase points, theoretically.\n",
201+
"Within the listening area, these points are found on rays that start at the virtual point source and intersect with active loudspeakers.\n",
202+
"The chosen points together shall shape a smooth contour, i.e. the reference contour.\n",
203+
"\n",
204+
"The example below shows a reference point xref that does not meet any ray (the gray lines in the level plot) alongside the stationary phase holds with its corresponding loudspeaker.\n",
205+
"\n",
206+
"The single point referencing scheme results in 0 dB isobar curve that closely passes the chosen xref point.\n",
207+
"In practice this typically works with sufficient precision once the position of xref is appropriately chosen (i.e. not too close, not too far, not to off-center from the active loudspeakers etc.)."
208+
]
209+
},
210+
{
211+
"cell_type": "code",
212+
"execution_count": null,
213+
"metadata": {},
214+
"outputs": [],
215+
"source": [
216+
"xref = [0, 0.1175, 0] # intentionally no stationary phase point\n",
217+
"# we don't forget to normalize the point source's amplitude\n",
218+
"# to this new reference point:\n",
219+
"normalize_gain = 4 * np.pi * np.linalg.norm(xs - np.array(xref))\n",
220+
"d, selection, secondary_source = sfs.fd.wfs.point_25d(\n",
221+
" omega, array.x, array.n, xs, xref=xref)\n",
222+
"p_point = sound_field(\n",
223+
" d * normalize_gain, selection, array, secondary_source,\n",
224+
" grid, np.array([xref]*array.x.shape[0]))\n",
225+
"\n",
226+
"# plot stationary phase rays\n",
227+
"# one ray connects the virtual source with one activate loudspeaker\n",
228+
"spa = array.x + 3*R * normalize_rows(array.x - xs)\n",
229+
"plt.plot(\n",
230+
" np.vstack((array.x[selection, 0], spa[selection, 0])),\n",
231+
" np.vstack((array.x[selection, 1], spa[selection, 1])),\n",
232+
" color='gray')\n",
233+
"plt.xlim(-2, 2)\n",
234+
"plt.ylim(-2, 2);\n"
235+
]
236+
},
237+
{
238+
"cell_type": "markdown",
239+
"metadata": {},
240+
"source": [
241+
"A plane wave like sound field, e.g. by setting `xs = -100, 0, 0`, for all above examples reveals some further interesting implications of the different referencing schemes."
242+
]
243+
}
244+
],
245+
"metadata": {
246+
"kernelspec": {
247+
"display_name": "sfs",
248+
"language": "python",
249+
"name": "python3"
250+
},
251+
"language_info": {
252+
"codemirror_mode": {
253+
"name": "ipython",
254+
"version": 3
255+
},
256+
"file_extension": ".py",
257+
"mimetype": "text/x-python",
258+
"name": "python",
259+
"nbconvert_exporter": "python",
260+
"pygments_lexer": "ipython3",
261+
"version": "3.13.7"
262+
}
263+
},
264+
"nbformat": 4,
265+
"nbformat_minor": 2
266+
}

doc/references.bib

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,10 @@ @phdthesis{Schultz2016
9696
year = {2016},
9797
doi = {10.18453/rosdok_id00001765}
9898
}
99+
@phdthesis{Firtha2019,
100+
author = {Firtha, G.},
101+
title = {{A Generalized Wave Field Synthesis Framework with Application
102+
for Moving Virtual Sources}},
103+
school = {Budapest University of Technology and Economics},
104+
year = {2019}
105+
}

sfs/fd/wfs.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
205205
206206
is implemented.
207207
The theoretical link of `point_25d()` and `point_25d_legacy()` was
208-
introduced as *unified WFS framework* in :cite:`Firtha2017`.
208+
introduced as *unified WFS framework* in :cite:`Firtha2017`, :cite:`Firtha2019`.
209209
210210
Examples
211211
--------
@@ -217,6 +217,12 @@ def point_25d(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
217217
normalize_gain = 4 * np.pi * np.linalg.norm(xs)
218218
plot(normalize_gain * d, selection, secondary_source)
219219
220+
.. nblinkgallery::
221+
:caption: Further Example
222+
:name: wfs-referencing-link-gallery
223+
224+
examples/wfs-referencing
225+
220226
"""
221227
x0 = _util.asarray_of_rows(x0)
222228
n0 = _util.asarray_of_rows(n0)
@@ -292,7 +298,7 @@ def point_25d_legacy(omega, x0, n0, xs, xref=[0, 0, 0], c=None, omalias=None):
292298
\e{-\i\wc |\x_0-\x_\text{s}|}
293299
294300
The theoretical link of `point_25d()` and `point_25d_legacy()` was
295-
introduced as *unified WFS framework* in :cite:`Firtha2017`.
301+
introduced as *unified WFS framework* in :cite:`Firtha2017`, :cite:`Firtha2019`.
296302
Also cf. Eq. (2.145)-(2.147) :cite:`Schultz2016`.
297303
298304
Examples

0 commit comments

Comments
 (0)