1use crate::error::SampleError;
7use crate::physics::Solver;
8
9use crate::physics::bound::Bound::*;
10use crate::physics::bound::all;
11use crate::physics::hammer::Hammer;
12
13#[derive(Clone, Debug, PartialEq)]
14pub struct ChaigneAskenfeltParams {
15 pub f0: f64,
16 pub b: f64,
17 pub strike_pos: f64,
18 pub vel: f64,
19 pub hammer_mass: f64,
20 pub hammer_k: f64,
21 pub hammer_p: f64,
22 pub damp_dc: f64,
23 pub damp_freq: f64,
24 pub unison_count: f64,
25 pub detune: f64,
26 pub bridge_coupling: f64,
27 pub bridge_mass: f64,
29 pub string_cents: [f64; MAX_UNISON],
31 pub string_hammer_k_ratio: [f64; MAX_UNISON],
32}
33
34impl ChaigneAskenfeltParams {
35 pub fn at(f0: f64) -> ChaigneAskenfeltParams {
37 ChaigneAskenfeltParams {
38 f0,
39 b: 0.00021,
40 strike_pos: 0.125,
41 vel: 3.2,
42 hammer_mass: 2.9e-3,
43 hammer_k: 2.6646e8,
44 hammer_p: 2.5,
45 damp_dc: 0.6,
46 damp_freq: 1.6e-4,
47 unison_count: 1.0,
48 detune: 2f64.powf(3.0 / 1200.0),
49 bridge_coupling: 1000.0,
50 bridge_mass: 0.0,
51 string_cents: [0.0; MAX_UNISON],
52 string_hammer_k_ratio: [1.0; MAX_UNISON],
53 }
54 }
55}
56
57impl ChaigneAskenfeltParams {
58 pub fn valid(&self) -> bool {
59 let [c1, c2, c3] = self.string_cents;
60 let [k1, k2, k3] = self.string_hammer_k_ratio;
61 all(&[
62 (self.f0, Positive),
63 (self.b, NonNegative),
64 (self.strike_pos, OpenUnit),
65 (self.vel, Positive),
66 (self.hammer_mass, Positive),
67 (self.hammer_k, Positive),
68 (self.hammer_p, Positive),
69 (self.damp_dc, NonNegative),
70 (self.damp_freq, NonNegative),
71 (self.unison_count, Within(1.0, MAX_UNISON as f64)),
72 (self.detune, AtLeast(1.0)),
73 (self.bridge_coupling, Positive),
74 (self.bridge_mass, NonNegative),
75 (c1, Finite),
76 (c2, Finite),
77 (c3, Finite),
78 (k1, Positive),
79 (k2, Positive),
80 (k3, Positive),
81 ])
82 }
83}
84
85const WIRE_DENSITY_KG_M3: f64 = 7850.0;
86const WIRE_RADIUS_M: f64 = 0.6e-3;
88const STRING_TENSION_N: f64 = 1500.0;
90#[derive(Clone, Copy)]
93enum Termination {
94 Rigid,
95 SharedBridge,
96}
97
98pub(crate) struct StringGrid {
99 pub(crate) y_now: Vec<f64>,
100 pub(crate) y_prev: Vec<f64>,
101 pub(crate) y_next: Vec<f64>,
102 pub(crate) n: usize,
103 pub(crate) dx: f64,
104 pub(crate) rho: f64,
105 pub(crate) courant_sq: f64,
106 pub(crate) stiff_sq: f64,
107 pub(crate) damp_a: f64,
108 pub(crate) damp_b: f64,
109 far_termination: Termination,
110}
111
112pub(crate) fn ghost_pinned(y: &[f64], n: usize, idx: isize, pin: f64) -> f64 {
114 if idx < 0 {
115 -y[(-idx) as usize]
116 } else if idx as usize > n {
117 2.0 * pin - y[(2 * n as isize - idx) as usize]
118 } else {
119 y[idx as usize]
120 }
121}
122
123pub(crate) fn stencil_update(grid: &StringGrid, j: usize, pin_now: f64, pin_prev: f64) -> f64 {
125 let n = grid.n;
126 let jj = j as isize;
127 let lap_now = ghost_pinned(&grid.y_now, n, jj + 1, pin_now) - 2.0 * grid.y_now[j]
128 + ghost_pinned(&grid.y_now, n, jj - 1, pin_now);
129 let biharm = ghost_pinned(&grid.y_now, n, jj + 2, pin_now)
130 - 4.0 * ghost_pinned(&grid.y_now, n, jj + 1, pin_now)
131 + 6.0 * grid.y_now[j]
132 - 4.0 * ghost_pinned(&grid.y_now, n, jj - 1, pin_now)
133 + ghost_pinned(&grid.y_now, n, jj - 2, pin_now);
134 let lap_prev = ghost_pinned(&grid.y_prev, n, jj + 1, pin_prev) - 2.0 * grid.y_prev[j]
135 + ghost_pinned(&grid.y_prev, n, jj - 1, pin_prev);
136 2.0 * grid.y_now[j] - grid.y_prev[j] + grid.courant_sq * lap_now
137 - grid.stiff_sq * biharm
138 - grid.damp_a * (grid.y_now[j] - grid.y_prev[j])
139 + grid.damp_b * (lap_now - lap_prev)
140}
141
142pub(crate) fn stiff_string_grid(
143 rho: f64,
144 c: f64,
145 length: f64,
146 kappa: f64,
147 damp_dc: f64,
148 damp_freq: f64,
149 sr: f64,
150) -> StringGrid {
151 let dt = 1.0 / sr;
152 let n = finest_stable_points(c, length, kappa, dt).max(4);
153 let dx = length / n as f64;
154 let (c2, dt2, k2) = (c * c, dt * dt, kappa * kappa);
155 let mut grid = lossy_grid(n, rho, length, damp_dc, damp_freq, dt);
156 grid.courant_sq = c2 * dt2 / (dx * dx);
157 grid.stiff_sq = k2 * dt2 / dx.powi(4);
158 grid
159}
160
161fn finest_stable_points(c: f64, length: f64, kappa: f64, dt: f64) -> usize {
163 let (c2, dt2, k2) = (c * c, dt * dt, kappa * kappa);
164 let dx_bound = ((c2 * dt2 + (c2 * c2 * dt2 * dt2 + 16.0 * k2 * dt2).sqrt()) / 2.0).sqrt();
165 (length / dx_bound).floor() as usize
166}
167
168fn lossy_grid(
170 n: usize,
171 rho: f64,
172 length: f64,
173 damp_dc: f64,
174 damp_freq: f64,
175 dt: f64,
176) -> StringGrid {
177 let dx = length / n as f64;
178 StringGrid {
179 y_now: vec![0.0; n + 1],
180 y_prev: vec![0.0; n + 1],
181 y_next: vec![0.0; n + 1],
182 n,
183 dx,
184 rho,
185 courant_sq: 0.0,
186 stiff_sq: 0.0,
187 damp_a: 2.0 * damp_dc * dt,
188 damp_b: 2.0 * damp_freq * dt / (dx * dx),
189 far_termination: Termination::Rigid,
190 }
191}
192
193fn mode_s(m: usize, n: usize) -> f64 {
195 (m as f64 * std::f64::consts::PI / (2.0 * n as f64))
196 .sin()
197 .powi(2)
198}
199
200fn mode_sigma(grid: &StringGrid, s: f64) -> f64 {
201 grid.damp_a + 4.0 * grid.damp_b * s
202}
203
204fn stiffness_for(theta: f64, sigma: f64) -> f64 {
206 let r = (1.0 - sigma).sqrt();
207 (sigma / (1.0 + r)).powi(2) + 4.0 * r * (theta / 2.0).sin().powi(2)
208}
209
210fn is_stable(grid: &StringGrid) -> bool {
212 (1..grid.n).all(|m| {
213 let s = mode_s(m, grid.n);
214 let sigma = mode_sigma(grid, s);
215 let d = 4.0 * grid.courant_sq * s + 16.0 * grid.stiff_sq * s * s;
216 (0.0..2.0).contains(&sigma) && d > 0.0 && d < 4.0 - 2.0 * sigma
217 })
218}
219
220fn build_grid(params: &ChaigneAskenfeltParams, f0: f64, sr: f64) -> Option<StringGrid> {
223 let dt = 1.0 / sr;
224 let rho = std::f64::consts::PI * WIRE_RADIUS_M * WIRE_RADIUS_M * WIRE_DENSITY_KG_M3;
225 let c = (STRING_TENSION_N / rho).sqrt();
226 let length = c / (2.0 * f0);
227 let kappa = c * length * params.b.sqrt() / std::f64::consts::PI;
228 let theta = |k: f64| std::f64::consts::TAU * f0 * k * (1.0 + params.b * k * k).sqrt() * dt;
229 let (theta_1, theta_2) = (theta(1.0), theta(2.0));
230 if theta_2 >= std::f64::consts::PI {
231 return None;
232 }
233 (3..=finest_stable_points(c, length, kappa, dt))
234 .rev()
235 .find_map(|n| {
236 let mut grid = lossy_grid(n, rho, length, params.damp_dc, params.damp_freq, dt);
237 let (s1, s2) = (mode_s(1, n), mode_s(2, n));
238 let (sigma_1, sigma_2) = (mode_sigma(&grid, s1), mode_sigma(&grid, s2));
239 if sigma_1 >= 1.0 || sigma_2 >= 1.0 {
240 return None;
241 }
242 let (d1, d2) = (
243 stiffness_for(theta_1, sigma_1),
244 stiffness_for(theta_2, sigma_2),
245 );
246 let det = s1 * s2 * (s2 - s1);
248 grid.courant_sq = (d1 * s2 * s2 - d2 * s1 * s1) / (4.0 * det);
249 grid.stiff_sq = (s1 * d2 - s2 * d1) / (16.0 * det);
250 (grid.courant_sq > 0.0 && is_stable(&grid)).then_some(grid)
251 })
252}
253
254fn point_weights(n: usize, x: f64) -> Vec<f64> {
256 let nf = n as f64;
257 let cos_sum = |theta: f64| {
259 let half = theta / 2.0;
260 let sh = half.sin();
261 if sh == 0.0 {
262 nf - 1.0
263 } else {
264 (nf * half).sin() * ((nf - 1.0) * half).cos() / sh - 1.0
265 }
266 };
267 (0..=n)
268 .map(|j| match j {
269 0 => 0.0,
270 j if j == n => 0.0,
271 j => {
272 let xj = j as f64 / nf;
273 let pi = std::f64::consts::PI;
274 (cos_sum(pi * (x - xj)) - cos_sum(pi * (x + xj))) / nf
275 }
276 })
277 .collect()
278}
279
280fn unison_frequencies(f0: f64, detune: f64, unison_count: usize, cents: &[f64]) -> Vec<f64> {
283 let spread = detune.sqrt();
284 let placed = match unison_count {
285 1 => vec![f0],
286 2 => vec![f0 / spread, f0 * spread],
287 _ => vec![f0 / spread, f0, f0 * spread],
288 };
289 placed
290 .iter()
291 .zip(cents)
292 .map(|(&f, &c)| f * 2f64.powf(c / 1200.0))
293 .collect()
294}
295
296const MAX_UNISON: usize = 3;
298
299pub struct ChaigneAskenfeltSite {
300 strings: Vec<StringGrid>,
301 tensions: Vec<f64>,
303 strike: Vec<Vec<f64>>,
304 hammer: Hammer,
305 detached: Vec<bool>,
306 bridge_now: f64,
307 bridge_prev: f64,
308 bridge_coupling: f64,
309 bridge_mass: f64,
310 dt: f64,
311}
312
313impl ChaigneAskenfeltSite {
314 pub fn new(params: &ChaigneAskenfeltParams, sr: f64) -> Result<Self, SampleError> {
315 let unison_count = params.unison_count.round().clamp(1.0, MAX_UNISON as f64) as usize;
316 let freqs =
317 unison_frequencies(params.f0, params.detune, unison_count, ¶ms.string_cents);
318 let mut strings = freqs
319 .iter()
320 .map(|&f0| build_grid(params, f0, sr))
321 .collect::<Option<Vec<StringGrid>>>()
322 .ok_or(SampleError::StringPastRate {
323 model: "chaigne_askenfelt",
324 })?;
325 if strings.len() > 1 {
326 for grid in &mut strings {
327 grid.far_termination = Termination::SharedBridge;
328 }
329 }
330 let dt = 1.0 / sr;
331 let tensions = strings
332 .iter()
333 .map(|g| g.rho * g.courant_sq * g.dx * g.dx / (dt * dt))
334 .collect();
335 let strike = strings
336 .iter()
337 .map(|g| point_weights(g.n, params.strike_pos))
338 .collect();
339 Ok(ChaigneAskenfeltSite {
340 detached: vec![false; strings.len()],
341 strings,
342 tensions,
343 strike,
344 hammer: Hammer::new(
345 params.hammer_mass,
346 params.hammer_k,
347 params.hammer_p,
348 params.vel,
349 dt,
350 )
351 .with_anvil_ratios(¶ms.string_hammer_k_ratio[..unison_count]),
352 bridge_now: 0.0,
353 bridge_prev: 0.0,
354 bridge_coupling: params.bridge_coupling,
355 bridge_mass: params.bridge_mass,
356 dt,
357 })
358 }
359}
360
361impl Solver for ChaigneAskenfeltSite {
362 fn step(&mut self) -> f64 {
363 match self.strings[0].far_termination {
364 Termination::Rigid => self.step_single(),
365 Termination::SharedBridge => self.step_unison(),
366 }
367 }
368}
369
370impl ChaigneAskenfeltSite {
371 fn hammer_forces(&mut self, forces: &mut [f64]) {
373 let mut y_h = [0.0f64; MAX_UNISON];
374 for (i, grid) in self.strings.iter().enumerate() {
375 if !self.detached[i] {
376 y_h[i] = self.strike[i]
377 .iter()
378 .zip(&grid.y_now)
379 .map(|(w, y)| w * y)
380 .sum();
381 }
382 }
383 self.hammer.substeps(
384 self.dt,
385 &y_h[..self.strings.len()],
386 &mut self.detached,
387 forces,
388 );
389 }
390
391 fn step_single(&mut self) -> f64 {
392 let mut forces = [0.0f64; 1];
393 self.hammer_forces(&mut forces);
394 let grid = &mut self.strings[0];
395 let n = grid.n;
396 for i in 1..n {
397 grid.y_next[i] = stencil_update(grid, i, 0.0, 0.0);
398 }
399 spread(grid, &self.strike[0], forces[0], self.dt);
400 grid.y_next[0] = 0.0;
401 grid.y_next[n] = 0.0;
402
403 let sample = self.tensions[0] * (grid.y_now[n] - grid.y_now[n - 1]) / grid.dx;
404
405 std::mem::swap(&mut grid.y_prev, &mut grid.y_now);
406 std::mem::swap(&mut grid.y_now, &mut grid.y_next);
407
408 sample
409 }
410
411 fn step_unison(&mut self) -> f64 {
413 let mut forces = [0.0f64; MAX_UNISON];
414 let forces = &mut forces[..self.strings.len()];
415 self.hammer_forces(forces);
416
417 let (bridge_now, bridge_prev) = (self.bridge_now, self.bridge_prev);
418 let mut k_eff = 0.0;
420 let mut rhs_sum = 0.0;
421 let mut sample = 0.0;
422 for (i, &force) in forces.iter().enumerate() {
423 let grid = &mut self.strings[i];
424 let tension = self.tensions[i];
425 let n = grid.n;
426 for j in 1..n {
427 grid.y_next[j] = stencil_update(grid, j, bridge_now, bridge_prev);
428 }
429 spread(grid, &self.strike[i], force, self.dt);
430 grid.y_next[0] = 0.0;
431 k_eff += tension / grid.dx;
432 rhs_sum += tension * grid.y_now[n - 1] / grid.dx;
433 sample += tension * (grid.y_now[n] - grid.y_now[n - 1]) / grid.dx;
434 }
435
436 let z_string = (self.tensions[0] * self.strings[0].rho).sqrt();
437 let r_bridge = self.bridge_coupling * z_string;
438 let r_over_dt = r_bridge / self.dt;
439 let m_over_dt2 = self.bridge_mass / (self.dt * self.dt);
440 let bridge_next =
441 (rhs_sum + r_over_dt * bridge_now + m_over_dt2 * (2.0 * bridge_now - bridge_prev))
442 / (k_eff + r_over_dt + m_over_dt2);
443 for grid in &mut self.strings {
444 let n = grid.n;
445 grid.y_next[n] = bridge_next;
446 }
447
448 self.bridge_prev = bridge_now;
449 self.bridge_now = bridge_next;
450
451 for grid in &mut self.strings {
452 std::mem::swap(&mut grid.y_prev, &mut grid.y_now);
453 std::mem::swap(&mut grid.y_now, &mut grid.y_next);
454 }
455
456 sample
457 }
458}
459
460fn spread(grid: &mut StringGrid, weights: &[f64], force: f64, dt: f64) {
462 if force == 0.0 {
463 return;
464 }
465 let scale = (dt * dt / (grid.rho * grid.dx)) * force;
466 for (y, w) in grid.y_next.iter_mut().zip(weights) {
467 *y += scale * w;
468 }
469}