1use crate::error::SampleError;
7use crate::physics::Solver;
8
9use crate::physics::bound::Bound::*;
10use crate::physics::bound::all;
11
12const SOUND_SPEED_M_S: f64 = 343.0;
13const AIR_DENSITY_KG_M3: f64 = 1.225;
14
15pub const NODE_COUNT_CEILING: usize = 250_000;
17
18#[derive(Clone, Debug, PartialEq)]
19pub struct BotteldoorenParams {
20 pub f0: f64,
21 pub aspect_y: f64,
22 pub aspect_z: f64,
23 pub listener_x: f64,
24 pub listener_y: f64,
25 pub listener_z: f64,
26 pub pulse_amp: f64,
27 pub pulse_width: f64,
28 pub damp_dc: f64,
29 pub damp_freq: f64,
30}
31
32impl BotteldoorenParams {
33 pub fn at(f0: f64) -> BotteldoorenParams {
35 BotteldoorenParams {
36 f0,
37 aspect_y: 0.75,
38 aspect_z: 0.6,
39 listener_x: 0.7,
40 listener_y: 0.3,
41 listener_z: 0.6,
42 pulse_amp: 1.0,
43 pulse_width: 0.001,
44 damp_dc: 0.5,
45 damp_freq: 1.0e-4,
46 }
47 }
48}
49
50impl BotteldoorenParams {
51 pub fn valid(&self) -> bool {
52 all(&[
53 (self.f0, Positive),
54 (self.aspect_y, Positive),
55 (self.aspect_z, Positive),
56 (self.listener_x, OpenUnit),
57 (self.listener_y, OpenUnit),
58 (self.listener_z, OpenUnit),
59 (self.pulse_amp, Positive),
60 (self.pulse_width, Positive),
61 (self.damp_dc, NonNegative),
62 (self.damp_freq, NonNegative),
63 ])
64 }
65}
66
67#[inline]
68fn node_index(nx: usize, ny: usize, ix: usize, iy: usize, iz: usize) -> usize {
69 (iz * (ny + 1) + iy) * (nx + 1) + ix
70}
71
72#[inline]
74fn mirror(i: isize, n: usize) -> usize {
75 if i < 0 {
76 1
77 } else if i as usize > n {
78 n - 1
79 } else {
80 i as usize
81 }
82}
83
84#[inline]
86fn laplacian(u: &[f64], nx: usize, ny: usize, nz: usize, ix: usize, iy: usize, iz: usize) -> f64 {
87 let c = node_index(nx, ny, ix, iy, iz);
88 let xm = node_index(nx, ny, mirror(ix as isize - 1, nx), iy, iz);
89 let xp = node_index(nx, ny, mirror(ix as isize + 1, nx), iy, iz);
90 let ym = node_index(nx, ny, ix, mirror(iy as isize - 1, ny), iz);
91 let yp = node_index(nx, ny, ix, mirror(iy as isize + 1, ny), iz);
92 let zm = node_index(nx, ny, ix, iy, mirror(iz as isize - 1, nz));
93 let zp = node_index(nx, ny, ix, iy, mirror(iz as isize + 1, nz));
94 (u[xp] - 2.0 * u[c] + u[xm]) + (u[yp] - 2.0 * u[c] + u[ym]) + (u[zp] - 2.0 * u[c] + u[zm])
95}
96
97fn room_sizing_f64(f0: f64, aspect_y: f64, aspect_z: f64, sr: f64) -> (f64, f64, f64, f64) {
99 let (nx_u, ny_u, nz_u, h) = axis_unrounded_nodes(f0, aspect_y, aspect_z, sr);
100 (
101 nx_u.round().max(4.0),
102 ny_u.round().max(4.0),
103 nz_u.round().max(4.0),
104 h,
105 )
106}
107
108fn axis_unrounded_nodes(f0: f64, aspect_y: f64, aspect_z: f64, sr: f64) -> (f64, f64, f64, f64) {
109 let dt = 1.0 / sr;
110 let lambda_max = 1.0 / 3.0f64.sqrt();
112 let lambda_target = 0.9 * lambda_max;
113 let h = SOUND_SPEED_M_S * dt / lambda_target;
114 let lx = SOUND_SPEED_M_S / (2.0 * f0);
116 let ly = lx * aspect_y;
117 let lz = lx * aspect_z;
118 (lx / h, ly / h, lz / h, h)
119}
120
121pub fn node_count(f0: f64, aspect_y: f64, aspect_z: f64, sr: f64) -> f64 {
122 let (nx, ny, nz, _) = room_sizing_f64(f0, aspect_y, aspect_z, sr);
123 (nx + 1.0) * (ny + 1.0) * (nz + 1.0)
124}
125
126pub const MIN_AXIS_UNROUNDED_NODES: f64 = 6.0;
128
129pub fn max_axis_unrounded_nodes(f0: f64, aspect_y: f64, aspect_z: f64, sr: f64) -> f64 {
130 let (nx_u, ny_u, nz_u, _) = axis_unrounded_nodes(f0, aspect_y, aspect_z, sr);
131 nx_u.max(ny_u).max(nz_u)
132}
133
134#[derive(Clone)]
135struct RoomGrid {
136 p_now: Vec<f64>,
137 p_prev: Vec<f64>,
138 p_next: Vec<f64>,
139 nx: usize,
140 ny: usize,
141 nz: usize,
142 h: f64,
143 rho0: f64,
144 courant_sq: f64,
146 damp_a: f64,
147 damp_b: f64,
148}
149
150fn build_grid(params: &BotteldoorenParams, sr: f64) -> RoomGrid {
152 let dt = 1.0 / sr;
153 let (nx, ny, nz, h) = room_sizing_f64(params.f0, params.aspect_y, params.aspect_z, sr);
154 let (nx, ny, nz) = (nx as usize, ny as usize, nz as usize);
155 let count = (nx + 1) * (ny + 1) * (nz + 1);
156 let lambda_target = 0.9 / 3.0f64.sqrt();
157 RoomGrid {
158 p_now: vec![0.0; count],
159 p_prev: vec![0.0; count],
160 p_next: vec![0.0; count],
161 nx,
162 ny,
163 nz,
164 h,
165 rho0: AIR_DENSITY_KG_M3,
166 courant_sq: lambda_target * lambda_target,
167 damp_a: 2.0 * params.damp_dc * dt,
168 damp_b: 2.0 * params.damp_freq * dt / (h * h),
169 }
170}
171
172const SOURCE_X: f64 = 0.21;
174const SOURCE_Y: f64 = 0.57;
175const SOURCE_Z: f64 = 0.34;
176
177const SOURCE_WINDOW_RADIUS_CELLS: f64 = 2.0;
179const SOURCE_WINDOW_WEIGHT_FLOOR: f64 = 1e-3;
180
181#[derive(Clone)]
182struct SourcePatch {
183 nodes: Vec<usize>,
184 weights: Vec<f64>,
185}
186
187fn build_source_patch(
188 nx: usize,
189 ny: usize,
190 nz: usize,
191 cx: usize,
192 cy: usize,
193 cz: usize,
194) -> SourcePatch {
195 let coeff = SOURCE_WINDOW_WEIGHT_FLOOR.ln().abs() / SOURCE_WINDOW_RADIUS_CELLS.powi(4);
196 let radius = SOURCE_WINDOW_RADIUS_CELLS.ceil() as isize;
197 let mut nodes = Vec::new();
198 let mut weights = Vec::new();
199 let mut total = 0.0;
200 for dk in -radius..=radius {
201 for dj in -radius..=radius {
202 for di in -radius..=radius {
203 let (ix, iy, iz) = (cx as isize + di, cy as isize + dj, cz as isize + dk);
204 if ix < 0
205 || ix > nx as isize
206 || iy < 0
207 || iy > ny as isize
208 || iz < 0
209 || iz > nz as isize
210 {
211 continue;
212 }
213 let quartic = |d: isize| (d * d * d * d) as f64;
214 let w = (-coeff * (quartic(di) + quartic(dj) + quartic(dk))).exp();
215 if w < SOURCE_WINDOW_WEIGHT_FLOOR {
216 continue;
217 }
218 nodes.push(node_index(nx, ny, ix as usize, iy as usize, iz as usize));
219 weights.push(w);
220 total += w;
221 }
222 }
223 }
224 for w in &mut weights {
225 *w /= total;
226 }
227 SourcePatch { nodes, weights }
228}
229
230#[derive(Clone)]
231pub struct BotteldoorenSite {
232 grid: RoomGrid,
233 source_patch: SourcePatch,
234 listener_index: usize,
235 pulse_amp: f64,
236 pulse_width: f64,
237 sr: f64,
238 sample_index: u64,
239}
240
241fn axis_index(ratio: f64, n: usize) -> usize {
242 (ratio * n as f64).round().clamp(0.0, n as f64) as usize
243}
244
245impl BotteldoorenSite {
246 pub fn new(params: &BotteldoorenParams, sr: f64) -> BotteldoorenSite {
247 let grid = build_grid(params, sr);
248 let (nx, ny, nz) = (grid.nx, grid.ny, grid.nz);
249 let source_patch = build_source_patch(
250 nx,
251 ny,
252 nz,
253 axis_index(SOURCE_X, nx),
254 axis_index(SOURCE_Y, ny),
255 axis_index(SOURCE_Z, nz),
256 );
257 let listener_index = node_index(
258 nx,
259 ny,
260 axis_index(params.listener_x, nx),
261 axis_index(params.listener_y, ny),
262 axis_index(params.listener_z, nz),
263 );
264 BotteldoorenSite {
265 grid,
266 source_patch,
267 listener_index,
268 pulse_amp: params.pulse_amp,
269 pulse_width: params.pulse_width,
270 sr,
271 sample_index: 0,
272 }
273 }
274}
275
276impl BotteldoorenSite {
277 fn advance(&mut self) -> f64 {
279 let (t, dt) = (self.sample_index as f64 / self.sr, 1.0 / self.sr);
280 let source = crate::physics::raised_cosine_pulse(t, self.pulse_amp, self.pulse_width);
281
282 let grid = &mut self.grid;
283 let (nx, ny, nz) = (grid.nx, grid.ny, grid.nz);
284
285 for iz in 0..=nz {
286 for iy in 0..=ny {
287 for ix in 0..=nx {
288 let c = node_index(nx, ny, ix, iy, iz);
289 let lap_now = laplacian(&grid.p_now, nx, ny, nz, ix, iy, iz);
290 let lap_prev = laplacian(&grid.p_prev, nx, ny, nz, ix, iy, iz);
291 grid.p_next[c] = 2.0 * grid.p_now[c] - grid.p_prev[c]
292 + grid.courant_sq * lap_now
293 - grid.damp_a * (grid.p_now[c] - grid.p_prev[c])
294 + grid.damp_b * (lap_now - lap_prev);
295 }
296 }
297 }
298
299 if source != 0.0 {
300 let injection = (dt * dt / (grid.rho0 * grid.h.powi(3))) * source;
302 for (&node, &w) in self
303 .source_patch
304 .nodes
305 .iter()
306 .zip(&self.source_patch.weights)
307 {
308 grid.p_next[node] += injection * w;
309 }
310 }
311
312 let sample = grid.p_now[self.listener_index];
313 self.sample_index += 1;
314
315 std::mem::swap(&mut grid.p_prev, &mut grid.p_now);
316 std::mem::swap(&mut grid.p_now, &mut grid.p_next);
317
318 sample
319 }
320}
321
322impl Solver for BotteldoorenSite {
323 fn bytes(&self) -> usize {
324 let (grid, patch) = (&self.grid, &self.source_patch);
325 size_of::<Self>()
326 + super::floats(&grid.p_now)
327 + super::floats(&grid.p_prev)
328 + super::floats(&grid.p_next)
329 + std::mem::size_of_val(patch.nodes.as_slice())
330 + super::floats(&patch.weights)
331 }
332
333 fn step(&mut self, _args: &[f64]) -> Result<f64, SampleError> {
334 Ok(self.advance())
335 }
336}