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