1use sva_ast::{FINITE_DIFFERENCE, MODAL};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub struct Meaning {
8 pub text: &'static str,
9 pub unit: &'static str,
10 pub part: Option<&'static str>,
11}
12
13const fn m(text: &'static str, unit: &'static str, part: &'static str) -> Meaning {
14 Meaning {
15 text,
16 unit,
17 part: Some(part),
18 }
19}
20
21const fn plain(text: &'static str, unit: &'static str) -> Meaning {
22 Meaning {
23 text,
24 unit,
25 part: None,
26 }
27}
28
29pub fn meaning(builtin: &str, key: &str) -> Option<Meaning> {
31 if crate::vocabulary::shape(builtin).is_some() {
32 return filter(key);
33 }
34 if FINITE_DIFFERENCE.contains(&builtin) {
35 return solver(builtin, key);
36 }
37 if MODAL.contains(&builtin) {
38 return modal(builtin, key);
39 }
40 Some(match (builtin, key) {
41 ("saw" | "square" | "triangle", "hz") => plain("the fundamental's frequency", "Hz"),
42 ("saw" | "square" | "triangle", "phase") => plain(
43 "the fundamental's phase offset; the nth harmonic turns n times as far, so the \
44 whole wave is read phase/(2*pi*hz) later",
45 "rad",
46 ),
47 ("saw" | "square" | "triangle", "tol") => plain(
48 "admitted and unused: the series is truncated where the profile's floor ends",
49 "none",
50 ),
51 ("sat", "drive") => plain("gain applied before the clip to [-1, 1]", "none"),
52 ("crop", "start") => plain("where the window opens, the second positional", "s"),
53 ("crop", "end") => plain("where the window closes, the third positional", "s"),
54 ("crop", "rise") => plain("raised-cosine fade-in from the window's start", "s"),
55 ("crop", "fall") => plain("raised-cosine fade-out into the window's end", "s"),
56 ("pwl" | "pwl_integral", "at") => plain("the time the table is read at", "s"),
57 ("pwl" | "pwl_integral", "t0" | "t1") => plain(
58 "a knot's time, never before the one ahead of it; every later pair is one more knot",
59 "s",
60 ),
61 ("pwl", "v0" | "v1") => plain("the curve's value at that knot", "none"),
62 ("pwl_integral", "v0" | "v1") => plain(
63 "the integrand's value at that knot, as hertz for a phase in cycles",
64 "none",
65 ),
66 ("rand", "seed") => plain("which hash the key is drawn through", "none"),
67 ("delta", "k") => plain("derivative order of the impulse", "none"),
68 ("stft", "window") => plain(
69 "frame length, a whole number of steps of the rate in use",
70 "s",
71 ),
72 ("stft", "hop") => plain(
73 "frame advance, a whole number of steps of the rate in use",
74 "s",
75 ),
76 ("noise", "period") => plain(
77 "repeat time; the lines fall every 1/period Hz, and the level a hertz holds stays",
78 "s",
79 ),
80 ("noise", "color") => plain(
81 "spectral tilt: the amplitude ratio from one octave to the next, written in db \
82 (`-3db` is pink), pivoting on 1 Hz; a bare number is that ratio, 1 flat",
83 "ratio per octave",
84 ),
85 _ => return None,
86 })
87}
88
89fn filter(key: &str) -> Option<Meaning> {
90 Some(match key {
91 "cutoff" => plain("corner or centre frequency", "Hz"),
92 "q" => plain(
93 "resonance: centre frequency over bandwidth, with no limit of its own; a sampled \
94 filter holds q, flagged, where its damping (sin(2*pi*cutoff/rate)/(2q) for a \
95 bandpass) leaves [2^-31, 2^22], inside which rounding errs it by under 2^-20: at \
96 48 kHz q reaches 2.8e6 at 20 Hz and 1.4e8 at 1 kHz",
97 "none",
98 ),
99 "gain" => plain(
100 "boost or cut of a shelf or peak: the amplitude ratio there, written in db (`12db`, \
101 `-6db`); a bare number is that ratio, 1 flat",
102 "ratio",
103 ),
104 _ => return None,
105 })
106}
107
108fn solver(builtin: &str, key: &str) -> Option<Meaning> {
111 let part = match builtin {
112 "chaigne_askenfelt" | "willemsen_bilbao_serafin" => "string",
113 "darabundit_scavone" => "bore",
114 "rhaouti_chaigne_joly" => "membrane",
115 "chaigne_doutaut" => "bar",
116 "botteldooren" => "room",
117 _ => return None,
118 };
119 if let Some(n) = numbered(key, "string") {
120 return match n.1 {
121 "_cents" => Some(m(
122 "string's offset from its unison placing",
123 "cents",
124 "string",
125 )),
126 "_hammer_k_ratio" => Some(m(
127 "felt stiffness this string meets, as a multiple of hammer_k",
128 "none",
129 "hammer",
130 )),
131 _ => None,
132 };
133 }
134 if let Some(n) = numbered(key, "hole") {
135 return match n.1 {
136 "_pos" => Some(m("tone hole position along the bore", "none", "tonehole")),
137 "_open" => Some(m("1 leaves the hole open, 0 closes it", "none", "tonehole")),
138 "_radius" => Some(m("tone hole radius", "m", "tonehole")),
139 "_height" => Some(m("tone hole chimney height", "m", "tonehole")),
140 _ => None,
141 };
142 }
143 Some(match (builtin, key) {
144 ("darabundit_scavone", "length") => m("bore length", "m", part),
145 (_, "f0") => m("fundamental frequency", "Hz", part),
146 ("darabundit_scavone", "damp_dc") => m("scale on the viscous wall loss", "none", part),
147 ("darabundit_scavone", "damp_freq") => m("scale on the thermal wall loss", "none", part),
148 (_, "damp_dc") => m("frequency-independent loss", "1/s", part),
149 (_, "damp_freq") => m("frequency-dependent loss", "m^2/s", part),
150 (_, "b") => m("string stiffness (inharmonicity)", "none", "string"),
151 (_, "strike_pos") => m(
152 "where the hammer strikes, along the length",
153 "none",
154 "hammer",
155 ),
156 (_, "strike_x") => m("where the hammer strikes, across x", "none", "hammer"),
157 (_, "strike_y") => m("where the hammer strikes, across y", "none", "hammer"),
158 (_, "vel") => m("hammer velocity at contact", "m/s", "hammer"),
159 (_, "hammer_mass") => m("hammer mass", "kg", "hammer"),
160 (_, "hammer_k") => m("felt stiffness K in F = K compression^p", "N/m^p", "hammer"),
161 (_, "hammer_p") => m("felt stiffness exponent p", "none", "hammer"),
162 (_, "unison_count") => m("strings struck together, 1 to 3", "none", "string"),
163 (_, "detune") => m(
164 "frequency ratio across the unison's outer strings",
165 "none",
166 "string",
167 ),
168 (_, "bridge_coupling") => m(
169 "bridge resistance, in string wave impedances",
170 "none",
171 "bridge",
172 ),
173 (_, "bridge_mass") => m("bridge mass; 0 is massless", "kg", "bridge"),
174 (_, "damper_pos") => m("where the felt presses, along the length", "none", "damper"),
175 (_, "damper_r") => m(
176 "felt dashpot pressing every string; 0 is lifted; may move every sample",
177 "N*s/m",
178 "damper",
179 ),
180 (_, "damper_k") => m(
181 "felt spring pressing every string; may only jump",
182 "N/m",
183 "damper",
184 ),
185 (_, "bow_pos") => m("where the bow touches, along the length", "none", "bow"),
186 (_, "bow_vel") => m("bow velocity; may move every sample", "m/s", "bow"),
187 (_, "bow_force") => m(
188 "force pressing the bow on the string; may move every sample",
189 "N",
190 "bow",
191 ),
192 (_, "mu_s") => m("static friction coefficient", "none", "friction"),
193 (_, "mu_c") => m("sliding (Coulomb) friction coefficient", "none", "friction"),
194 (_, "stribeck_vel") => m(
195 "slip speed over which static friction falls to sliding",
196 "m/s",
197 "friction",
198 ),
199 (_, "bristle_stiffness") => m("bristle stiffness s0", "N/m", "friction"),
200 (_, "bristle_damping") => m("bristle damping s1", "N*s/m", "friction"),
201 (_, "viscous_friction") => m("viscous friction s2", "N*s/m", "friction"),
202 (_, "radius_in") => m("bore radius at the excited end", "m", "bore"),
203 (_, "radius_out") => m("bore radius at the far end, a cone between", "m", "bore"),
204 (_, "excite_pos") => m("where the pulse enters, along the length", "none", "source"),
205 (_, "pulse_amp") => m("pressure pulse peak", "Pa", "source"),
206 (_, "pulse_width") => m("pressure pulse duration", "s", "source"),
207 (_, "aspect_ratio") => m("side ratio lx/ly at a fixed area", "none", "membrane"),
208 (_, "aspect_y") => m("room depth, in room lengths", "none", "room"),
209 (_, "aspect_z") => m("room height, in room lengths", "none", "room"),
210 (_, "listener_x") => m("listener position along the length", "none", "listener"),
211 (_, "listener_y") => m("listener position along the depth", "none", "listener"),
212 (_, "listener_z") => m("listener position along the height", "none", "listener"),
213 _ => return None,
214 })
215}
216
217fn numbered<'k>(key: &'k str, stem: &str) -> Option<(u32, &'k str)> {
219 let rest = key.strip_prefix(stem)?;
220 let digits = rest.find(|c: char| !c.is_ascii_digit())?;
221 Some((rest[..digits].parse().ok()?, &rest[digits..]))
222}
223
224fn modal(builtin: &str, key: &str) -> Option<Meaning> {
226 Some(match (builtin, key) {
227 ("hammer_pulse", "vel") => m("strike velocity", "m/s", "hammer"),
228 ("helmholtz", "volume") => m("cavity volume", "m^3", "cavity"),
229 ("string", "f0") => m("fundamental frequency", "Hz", "string"),
230 ("membrane" | "room", "lx") => m("side along x", "m", builtin_part(builtin)),
231 ("membrane" | "room", "ly") => m("side along y", "m", builtin_part(builtin)),
232 ("room", "lz") => m("side along z", "m", "room"),
233 ("bar" | "bore", "length") => m("length", "m", builtin_part(builtin)),
234 (_, "damp_dc") => m("decay rate every mode shares", "1/s", "damping"),
235 (_, "damp_freq") => m("decay rate per squared hertz", "s", "damping"),
236 (_, "modes") => plain("how many modes the bank holds", "none"),
237 (_, "vel") => m(
238 "strike velocity; omitted, an impulse rings the bank",
239 "m/s",
240 "hammer",
241 ),
242 (_, "at") => m("when the strike lands", "s", "hammer"),
243 (_, "contact") => m("contact time at 3.2 m/s", "s", "hammer"),
244 (_, "p") => m("felt stiffness exponent", "none", "hammer"),
245 (_, "force") => m("peak force at 3.2 m/s", "N", "hammer"),
246 ("string", "inharmonicity") => m("string stiffness B", "none", "string"),
247 ("string", "strike") => m("where the strike lands, along the length", "none", "hammer"),
248 ("membrane", "tension") => m("membrane tension", "N/m", "membrane"),
249 ("membrane", "density") => m("membrane areal density", "kg/m^2", "membrane"),
250 ("membrane", "strike_x") => m("where the strike lands, across x", "none", "hammer"),
251 ("membrane", "strike_y") => m("where the strike lands, across y", "none", "hammer"),
252 ("bar", "thickness") => m("bar thickness", "m", "bar"),
253 ("bar", "young") => m("Young's modulus", "Pa", "bar"),
254 ("bar", "density") => m("bar density", "kg/m^3", "bar"),
255 ("bore", "radius") => m("bore radius", "m", "bore"),
256 ("bore" | "room" | "helmholtz", "speed") => m("speed of sound", "m/s", "air"),
257 ("bore", "closed") => m("1 closes one end, 0 leaves both open", "none", "bore"),
258 ("helmholtz", "neck_area") => m("neck cross-section", "m^2", "neck"),
259 ("helmholtz", "neck_length") => m("neck length", "m", "neck"),
260 ("helmholtz", "radius") => m("neck radius, for the 1.7 r end correction", "m", "neck"),
261 _ => return None,
262 })
263}
264
265fn builtin_part(builtin: &str) -> &'static str {
266 match builtin {
267 "membrane" => "membrane",
268 "room" => "room",
269 "bar" => "bar",
270 _ => "bore",
271 }
272}