1pub mod bar;
4pub mod bore;
5pub mod damping;
6mod grid;
7pub mod membrane;
8pub mod room;
9pub mod string;
10
11use std::f64::consts::TAU;
12
13use crate::closed_form::{Edge, Excitation, ModalBank, Mode};
14use crate::complex::C64;
15use crate::modal::damping::Damping;
16use crate::origin::Origin;
17use crate::spectral_sum::atom::{Exp, Factors, Indicator, Poly, Singular, SpectralAtom};
18use crate::spectral_sum::image::raised_cosine;
19
20pub fn atoms(bank: &ModalBank, origin: Origin) -> Vec<SpectralAtom> {
22 let rings = bank.modes.iter().flat_map(|m| {
23 [1.0, -1.0].map(|sign| {
24 let onset = C64::new(0.0, sign * m.phase).exp().scale(m.amp * 0.5);
25 (onset, C64::new(-1.0 / m.tau, sign * m.omega))
26 })
27 });
28 match bank.excite {
29 Excitation::Impulse { t0 } => rings
30 .map(|(a, alpha)| {
31 onset_atom(
32 a,
33 alpha,
34 t0,
35 (Edge::at(t0), Edge::PosInf),
36 Poly::ONE,
37 origin,
38 )
39 })
40 .collect(),
41 Excitation::HammerPulse { f0, t0, contact } => {
42 struck(rings.collect(), Force { f0, t0, contact }, origin)
43 }
44 }
45}
46
47struct Force {
49 f0: f64,
50 t0: f64,
51 contact: f64,
52}
53
54impl Force {
55 fn lines(&self) -> [(C64, C64); 3] {
56 let w = TAU / self.contact;
57 let side = C64::real(-self.f0 * 0.25);
58 [
59 (C64::real(self.f0 * 0.5), C64::ZERO),
60 (side, C64::new(0.0, w)),
61 (side, C64::new(0.0, -w)),
62 ]
63 }
64}
65
66const NEAR: usize = 18;
68
69fn struck(rings: Vec<(C64, C64)>, force: Force, origin: Origin) -> Vec<SpectralAtom> {
72 let (t0, contact) = (force.t0, force.contact);
73 if contact <= 0.0 || !contact.is_finite() {
74 return Vec::new();
75 }
76 let t1 = t0 + contact;
77 let pressing = (Edge::at(t0), Edge::at(t1));
78 let lines = force.lines();
79 let mut driven = [C64::ZERO; 3];
80 let mut out = Vec::with_capacity(3 * rings.len() + lines.len());
81 for (a, alpha) in rings {
82 let ringing = alpha.scale(contact).exp();
83 let (mut head, mut released) = (C64::ZERO, C64::ZERO);
84 let mut near = [C64::ZERO; NEAR];
85 for (k, (q, beta)) in lines.iter().enumerate() {
86 let aq = a * *q;
87 let d = *beta - alpha;
88 released = released
89 + aq.scale(contact) * apart(beta.scale(contact), ringing, d.scale(contact));
90 if d.abs() * contact > 0.5 {
91 let over = aq / d;
92 driven[k] = driven[k] + over;
93 head = head - over;
94 continue;
95 }
96 let mut term = aq;
97 for (n, held) in near.iter_mut().enumerate() {
98 *held = *held + term;
99 term = (term * d).over((n + 2) as f64);
100 }
101 }
102 out.push(onset_atom(head, alpha, t0, pressing, Poly::ONE, origin));
103 for (n, c) in near.into_iter().enumerate() {
104 let power = Poly {
105 degree: (n + 1) as u16,
106 at: t0,
107 };
108 out.push(onset_atom(c, alpha, t0, pressing, power, origin));
109 }
110 let after = (Edge::at(t1), Edge::PosInf);
111 out.push(onset_atom(released, alpha, t1, after, Poly::ONE, origin));
112 }
113 for ((_, beta), c) in lines.iter().zip(driven) {
114 out.push(onset_atom(c, *beta, t0, pressing, Poly::ONE, origin));
115 }
116 out.retain(|atom| !atom.c.is_zero());
117 out
118}
119
120fn apart(beta_t: C64, ringing: C64, d_t: C64) -> C64 {
122 if d_t.re == 0.0 && d_t.im == 0.0 {
123 return ringing;
124 }
125 let lifted = match d_t.re.abs() <= 1.0 && d_t.im.abs() <= 1.0 {
126 true => ringing * d_t.exp_m1(),
127 false => beta_t.exp() - ringing,
128 };
129 lifted / d_t
130}
131
132fn onset_atom(
134 c: C64,
135 alpha: C64,
136 t0: f64,
137 (l, r): (Edge, Edge),
138 poly: Poly,
139 origin: Origin,
140) -> SpectralAtom {
141 let from_zero = c * (-alpha.scale(t0)).exp();
142 let (c, exp) = match (alpha.re == 0.0 && alpha.im == 0.0, from_zero.is_finite()) {
143 (true, _) => (c, None),
144 (false, true) => (from_zero, Some(Exp::at(alpha.re, alpha.im))),
145 (false, false) => (
146 c * C64::new(0.0, -alpha.im * t0).exp(),
147 Some(Exp {
148 sigma: alpha.re,
149 omega: alpha.im,
150 mu: t0,
151 }),
152 ),
153 };
154 SpectralAtom::new(
155 c,
156 Factors {
157 poly,
158 exp,
159 ind: Some(Indicator { l, r }),
160 ..Factors::NONE
161 },
162 Singular::Regular,
163 origin,
164 )
165}
166
167pub enum Geometry {
169 String(string::StringGeom),
170 Membrane(membrane::MembraneGeom),
171 Bar(bar::BarGeom),
172 Bore(bore::BoreGeom),
173 Room(room::RoomGeom),
174}
175
176pub fn mode(hz: f64, amp: f64, damping: Damping) -> Mode {
177 Mode {
178 omega: TAU * hz,
179 tau: damping.tau(hz),
180 amp,
181 phase: 0.0,
182 }
183}
184
185pub fn modes(g: &Geometry, count: usize) -> Vec<Mode> {
186 match g {
187 Geometry::String(s) => string::modes(s, count),
188 Geometry::Membrane(m) => membrane::modes(m, count),
189 Geometry::Bar(b) => bar::modes(b, count),
190 Geometry::Bore(b) => bore::modes(b, count),
191 Geometry::Room(r) => room::modes(r, count),
192 }
193}
194
195pub struct HammerGeom {
197 pub reference_velocity: f64,
198 pub reference_contact: f64,
199 pub stiffness_exponent: f64,
200 pub force: f64,
201}
202
203pub fn contact_time(velocity: f64, h: &HammerGeom) -> f64 {
204 let p = h.stiffness_exponent;
205 h.reference_contact * (velocity / h.reference_velocity).powf((1.0 - p) / (1.0 + p))
206}
207
208pub fn strike(velocity: f64, h: &HammerGeom, at: f64) -> Excitation {
210 let p = h.stiffness_exponent;
211 let ratio = velocity / h.reference_velocity;
212 Excitation::HammerPulse {
213 f0: h.force * ratio.powf(2.0 * p / (p + 1.0)),
214 t0: at,
215 contact: contact_time(velocity, h),
216 }
217}
218
219pub fn pulse(f0: f64, t0: f64, contact: f64, origin: Origin) -> Vec<SpectralAtom> {
221 if contact <= 0.0 || !contact.is_finite() {
222 return Vec::new();
223 }
224 let [(level, _), (side, turn), _] = Force { f0, t0, contact }.lines();
225 raised_cosine(t0, t0 + contact, turn.im, level.re, side.re, origin)
226}