1use crate::physics::Solver;
7
8use crate::physics::bound::Bound::*;
9use crate::physics::bound::all;
10use crate::physics::hammer::Hammer;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct ChaigneAskenfeltParams {
14 pub f0: f64,
15 pub b: f64,
16 pub strike_pos: f64,
17 pub vel: f64,
18 pub hammer_mass: f64,
19 pub hammer_k: f64,
20 pub hammer_p: f64,
21 pub damp_dc: f64,
22 pub damp_freq: f64,
23 pub unison_count: f64,
24 pub detune: f64,
25 pub bridge_coupling: f64,
26 pub bridge_mass: f64,
28 pub string_cents: [f64; MAX_UNISON],
30 pub string_hammer_k_ratio: [f64; MAX_UNISON],
31}
32
33impl ChaigneAskenfeltParams {
34 pub fn at(f0: f64) -> ChaigneAskenfeltParams {
36 ChaigneAskenfeltParams {
37 f0,
38 b: 0.00021,
39 strike_pos: 0.125,
40 vel: 3.2,
41 hammer_mass: 2.9e-3,
42 hammer_k: 2.6646e8,
43 hammer_p: 2.5,
44 damp_dc: 0.6,
45 damp_freq: 1.6e-4,
46 unison_count: 1.0,
47 detune: 2f64.powf(3.0 / 1200.0),
48 bridge_coupling: 1000.0,
49 bridge_mass: 0.0,
50 string_cents: [0.0; MAX_UNISON],
51 string_hammer_k_ratio: [1.0; MAX_UNISON],
52 }
53 }
54}
55
56impl ChaigneAskenfeltParams {
57 pub fn valid(&self) -> bool {
58 let [c1, c2, c3] = self.string_cents;
59 let [k1, k2, k3] = self.string_hammer_k_ratio;
60 all(&[
61 (self.f0, Positive),
62 (self.b, NonNegative),
63 (self.strike_pos, OpenUnit),
64 (self.vel, Positive),
65 (self.hammer_mass, Positive),
66 (self.hammer_k, Positive),
67 (self.hammer_p, Positive),
68 (self.damp_dc, NonNegative),
69 (self.damp_freq, NonNegative),
70 (self.unison_count, Within(1.0, MAX_UNISON as f64)),
71 (self.detune, AtLeast(1.0)),
72 (self.bridge_coupling, Positive),
73 (self.bridge_mass, NonNegative),
74 (c1, Finite),
75 (c2, Finite),
76 (c3, Finite),
77 (k1, Positive),
78 (k2, Positive),
79 (k3, Positive),
80 ])
81 }
82}
83
84const WIRE_DENSITY_KG_M3: f64 = 7850.0;
85const WIRE_RADIUS_M: f64 = 0.6e-3;
87const STRING_TENSION_N: f64 = 1500.0;
89#[derive(Clone, Copy)]
92enum Termination {
93 Rigid,
94 SharedBridge,
95}
96
97pub(crate) struct StringGrid {
98 pub(crate) y_now: Vec<f64>,
99 pub(crate) y_prev: Vec<f64>,
100 pub(crate) y_next: Vec<f64>,
101 pub(crate) n: usize,
102 pub(crate) dx: f64,
103 pub(crate) rho: f64,
104 pub(crate) courant_sq: f64,
105 pub(crate) stiff_sq: f64,
106 pub(crate) damp_a: f64,
107 pub(crate) damp_b: f64,
108 far_termination: Termination,
109}
110
111pub(crate) fn ghost_pinned(y: &[f64], n: usize, idx: isize, pin: f64) -> f64 {
113 if idx < 0 {
114 -y[(-idx) as usize]
115 } else if idx as usize > n {
116 2.0 * pin - y[(2 * n as isize - idx) as usize]
117 } else {
118 y[idx as usize]
119 }
120}
121
122pub(crate) fn stencil_update(grid: &StringGrid, j: usize, pin_now: f64, pin_prev: f64) -> f64 {
124 let n = grid.n;
125 let jj = j as isize;
126 let lap_now = ghost_pinned(&grid.y_now, n, jj + 1, pin_now) - 2.0 * grid.y_now[j]
127 + ghost_pinned(&grid.y_now, n, jj - 1, pin_now);
128 let biharm = ghost_pinned(&grid.y_now, n, jj + 2, pin_now)
129 - 4.0 * ghost_pinned(&grid.y_now, n, jj + 1, pin_now)
130 + 6.0 * grid.y_now[j]
131 - 4.0 * ghost_pinned(&grid.y_now, n, jj - 1, pin_now)
132 + ghost_pinned(&grid.y_now, n, jj - 2, pin_now);
133 let lap_prev = ghost_pinned(&grid.y_prev, n, jj + 1, pin_prev) - 2.0 * grid.y_prev[j]
134 + ghost_pinned(&grid.y_prev, n, jj - 1, pin_prev);
135 2.0 * grid.y_now[j] - grid.y_prev[j] + grid.courant_sq * lap_now
136 - grid.stiff_sq * biharm
137 - grid.damp_a * (grid.y_now[j] - grid.y_prev[j])
138 + grid.damp_b * (lap_now - lap_prev)
139}
140
141pub(crate) fn stiff_string_grid(
142 rho: f64,
143 c: f64,
144 length: f64,
145 kappa: f64,
146 damp_dc: f64,
147 damp_freq: f64,
148 sr: f64,
149) -> StringGrid {
150 let dt = 1.0 / sr;
151 let (c2, dt2, k2) = (c * c, dt * dt, kappa * kappa);
152 let dx_bound = ((c2 * dt2 + (c2 * c2 * dt2 * dt2 + 16.0 * k2 * dt2).sqrt()) / 2.0).sqrt();
153 let n = ((length / dx_bound).floor() as usize).max(4);
154 let dx = length / n as f64;
155
156 StringGrid {
157 y_now: vec![0.0; n + 1],
158 y_prev: vec![0.0; n + 1],
159 y_next: vec![0.0; n + 1],
160 n,
161 dx,
162 rho,
163 courant_sq: c2 * dt2 / (dx * dx),
164 stiff_sq: k2 * dt2 / dx.powi(4),
165 damp_a: 2.0 * damp_dc * dt,
166 damp_b: 2.0 * damp_freq * dt / (dx * dx),
167 far_termination: Termination::Rigid,
168 }
169}
170
171fn build_grid(params: &ChaigneAskenfeltParams, f0: f64, sr: f64) -> StringGrid {
174 let rho = std::f64::consts::PI * WIRE_RADIUS_M * WIRE_RADIUS_M * WIRE_DENSITY_KG_M3;
175 let c = (STRING_TENSION_N / rho).sqrt();
176 let length = c / (2.0 * f0);
177 let kappa = c * length * params.b.sqrt() / std::f64::consts::PI;
178 stiff_string_grid(rho, c, length, kappa, params.damp_dc, params.damp_freq, sr)
179}
180
181fn unison_frequencies(f0: f64, detune: f64, unison_count: usize, cents: &[f64]) -> Vec<f64> {
184 let spread = detune.sqrt();
185 let placed = match unison_count {
186 1 => vec![f0],
187 2 => vec![f0 / spread, f0 * spread],
188 _ => vec![f0 / spread, f0, f0 * spread],
189 };
190 placed
191 .iter()
192 .zip(cents)
193 .map(|(&f, &c)| f * 2f64.powf(c / 1200.0))
194 .collect()
195}
196
197const MAX_UNISON: usize = 3;
199
200pub struct ChaigneAskenfeltSite {
201 strings: Vec<StringGrid>,
202 hammer: Hammer,
203 detached: bool,
204 contact_index: usize,
205 contact_indices: Vec<usize>,
206 strings_detached: Vec<bool>,
207 bridge_now: f64,
208 bridge_prev: f64,
209 bridge_coupling: f64,
210 bridge_mass: f64,
211 tension: f64,
212 dt: f64,
213}
214
215impl ChaigneAskenfeltSite {
216 pub fn new(params: &ChaigneAskenfeltParams, sr: f64) -> ChaigneAskenfeltSite {
217 let unison_count = params.unison_count.round().clamp(1.0, MAX_UNISON as f64) as usize;
218 let freqs =
219 unison_frequencies(params.f0, params.detune, unison_count, ¶ms.string_cents);
220 let mut strings: Vec<StringGrid> =
221 freqs.iter().map(|&f0| build_grid(params, f0, sr)).collect();
222 if strings.len() > 1 {
223 for grid in &mut strings {
224 grid.far_termination = Termination::SharedBridge;
225 }
226 }
227 let contact_indices: Vec<usize> = strings
228 .iter()
229 .map(|g| {
230 (params.strike_pos * g.n as f64)
231 .round()
232 .clamp(1.0, (g.n - 1) as f64) as usize
233 })
234 .collect();
235 let contact_index = contact_indices[0];
236 let strings_detached = vec![false; strings.len()];
237 let dt = 1.0 / sr;
238 ChaigneAskenfeltSite {
239 strings,
240 hammer: Hammer::new(
241 params.hammer_mass,
242 params.hammer_k,
243 params.hammer_p,
244 params.vel,
245 dt,
246 )
247 .with_anvil_ratios(¶ms.string_hammer_k_ratio[..unison_count]),
248 detached: false,
249 contact_index,
250 contact_indices,
251 strings_detached,
252 bridge_now: 0.0,
253 bridge_prev: 0.0,
254 bridge_coupling: params.bridge_coupling,
255 bridge_mass: params.bridge_mass,
256 tension: STRING_TENSION_N,
257 dt,
258 }
259 }
260}
261
262impl Solver for ChaigneAskenfeltSite {
263 fn step(&mut self) -> f64 {
264 match self.strings[0].far_termination {
265 Termination::Rigid => self.step_single(),
266 Termination::SharedBridge => self.step_unison(),
267 }
268 }
269}
270
271impl ChaigneAskenfeltSite {
272 fn step_single(&mut self) -> f64 {
274 let grid = &mut self.strings[0];
275 let n = grid.n;
276 let y_h = grid.y_now[self.contact_index];
277
278 let mut forces = [0.0f64; 1];
279 self.hammer.substeps(
280 self.dt,
281 &[y_h],
282 std::slice::from_mut(&mut self.detached),
283 &mut forces,
284 );
285 let force = forces[0];
286
287 for i in 1..n {
288 let mut next = stencil_update(grid, i, 0.0, 0.0);
289 if i == self.contact_index {
290 next += (self.dt * self.dt / (grid.rho * grid.dx)) * force;
291 }
292 grid.y_next[i] = next;
293 }
294 grid.y_next[0] = 0.0;
295 grid.y_next[n] = 0.0;
296
297 let sample = self.tension * (grid.y_now[n] - grid.y_now[n - 1]) / grid.dx;
298
299 std::mem::swap(&mut grid.y_prev, &mut grid.y_now);
300 std::mem::swap(&mut grid.y_now, &mut grid.y_next);
301
302 sample
303 }
304
305 fn step_unison(&mut self) -> f64 {
307 let count = self.strings.len();
308
309 let y_h: Vec<f64> = (0..count)
310 .map(|i| self.strings[i].y_now[self.contact_indices[i]])
311 .collect();
312
313 let mut forces = [0.0f64; MAX_UNISON];
314 let forces = &mut forces[..count];
315 self.hammer
316 .substeps(self.dt, &y_h, &mut self.strings_detached, forces);
317
318 let (bridge_now, bridge_prev) = (self.bridge_now, self.bridge_prev);
319 let mut k_eff = 0.0;
321 let mut rhs_sum = 0.0;
322 let mut sample = 0.0;
323 for (i, &force) in forces.iter().enumerate() {
324 let grid = &mut self.strings[i];
325 let n = grid.n;
326 for j in 1..n {
327 let mut next = stencil_update(grid, j, bridge_now, bridge_prev);
328 if j == self.contact_indices[i] {
329 next += (self.dt * self.dt / (grid.rho * grid.dx)) * force;
330 }
331 grid.y_next[j] = next;
332 }
333 grid.y_next[0] = 0.0;
334 k_eff += self.tension / grid.dx;
335 rhs_sum += self.tension * grid.y_now[n - 1] / grid.dx;
336 sample += self.tension * (grid.y_now[n] - grid.y_now[n - 1]) / grid.dx;
337 }
338
339 let z_string = (self.tension * self.strings[0].rho).sqrt();
340 let r_bridge = self.bridge_coupling * z_string;
341 let r_over_dt = r_bridge / self.dt;
342 let m_over_dt2 = self.bridge_mass / (self.dt * self.dt);
343 let bridge_next =
344 (rhs_sum + r_over_dt * bridge_now + m_over_dt2 * (2.0 * bridge_now - bridge_prev))
345 / (k_eff + r_over_dt + m_over_dt2);
346 for grid in &mut self.strings {
347 let n = grid.n;
348 grid.y_next[n] = bridge_next;
349 }
350
351 self.bridge_prev = bridge_now;
352 self.bridge_now = bridge_next;
353
354 for grid in &mut self.strings {
355 std::mem::swap(&mut grid.y_prev, &mut grid.y_now);
356 std::mem::swap(&mut grid.y_now, &mut grid.y_next);
357 }
358
359 sample
360 }
361}