Skip to main content

sva_engine/
meaning.rs

1// Concern: what each builtin's named arguments mean, their unit and the model part each moves | Non-concern: which names a builtin takes (vocabulary.rs) | IO: (builtin, name) -> Meaning
2
3use sva_formula::filter::Shape;
4
5use crate::lower::physics::MODAL;
6use crate::overload::FINITE_DIFFERENCE;
7
8/// `unit` is `none` for a pure number.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct Meaning {
11    pub text: &'static str,
12    pub unit: &'static str,
13    pub part: Option<&'static str>,
14}
15
16const fn m(text: &'static str, unit: &'static str, part: &'static str) -> Meaning {
17    Meaning {
18        text,
19        unit,
20        part: Some(part),
21    }
22}
23
24const fn plain(text: &'static str, unit: &'static str) -> Meaning {
25    Meaning {
26        text,
27        unit,
28        part: None,
29    }
30}
31
32/// `None` for a name `builtin` does not take, by name or, for a solver or a bank, by position.
33pub fn meaning(builtin: &str, key: &str) -> Option<Meaning> {
34    if Shape::from_name(builtin).is_some() {
35        return filter(key);
36    }
37    if FINITE_DIFFERENCE.contains(&builtin) {
38        return solver(builtin, key);
39    }
40    if MODAL.contains(&builtin) {
41        return modal(builtin, key);
42    }
43    Some(match (builtin, key) {
44        ("saw" | "square" | "triangle", "tol") => plain(
45            "admitted and unused: the series is truncated where the profile's floor ends",
46            "none",
47        ),
48        ("sat", "drive") => plain("gain applied before the clip to [-1, 1]", "none"),
49        ("crop", "start") => plain("where the window opens, the second positional", "s"),
50        ("crop", "end") => plain("where the window closes, the third positional", "s"),
51        ("crop", "rise") => plain("raised-cosine fade-in from the window's start", "s"),
52        ("crop", "fall") => plain("raised-cosine fade-out into the window's end", "s"),
53        ("rand", "seed") => plain("which hash the key is drawn through", "none"),
54        ("delta", "k") => plain("derivative order of the impulse", "none"),
55        ("stft", "window") => plain("frame length", "samples"),
56        ("stft", "hop") => plain("frame advance", "samples"),
57        ("noise", "period") => plain("repeat time; the lines fall every 1/period Hz", "s"),
58        ("noise", "color") => plain("spectral tilt of the lines", "dB/octave"),
59        _ => return None,
60    })
61}
62
63fn filter(key: &str) -> Option<Meaning> {
64    Some(match key {
65        "cutoff" => plain("corner or centre frequency", "Hz"),
66        "q" => plain("resonance: centre frequency over bandwidth", "none"),
67        "gain" => plain("boost or cut of a shelf or peak", "dB"),
68        _ => return None,
69    })
70}
71
72/// A finite-difference model's losses act on its own grid: `damp_dc` on velocity,
73/// `damp_freq` on the rate of curvature, except the bore's, which scale two loss families.
74fn solver(builtin: &str, key: &str) -> Option<Meaning> {
75    let part = match builtin {
76        "chaigne_askenfelt" | "willemsen_bilbao_serafin" => "string",
77        "darabundit_scavone" => "bore",
78        "rhaouti_chaigne_joly" => "membrane",
79        "chaigne_doutaut" => "bar",
80        "botteldooren" => "room",
81        _ => return None,
82    };
83    if let Some(n) = numbered(key, "string") {
84        return match n.1 {
85            "_cents" => Some(m(
86                "string's offset from its unison placing",
87                "cents",
88                "string",
89            )),
90            "_hammer_k_ratio" => Some(m(
91                "felt stiffness this string meets, as a multiple of hammer_k",
92                "none",
93                "hammer",
94            )),
95            _ => None,
96        };
97    }
98    if let Some(n) = numbered(key, "hole") {
99        return match n.1 {
100            "_pos" => Some(m("tone hole position along the bore", "none", "tonehole")),
101            "_open" => Some(m("1 leaves the hole open, 0 closes it", "none", "tonehole")),
102            "_radius" => Some(m("tone hole radius", "m", "tonehole")),
103            "_height" => Some(m("tone hole chimney height", "m", "tonehole")),
104            _ => None,
105        };
106    }
107    Some(match (builtin, key) {
108        ("darabundit_scavone", "length") => m("bore length", "m", part),
109        (_, "f0") => m("fundamental frequency", "Hz", part),
110        ("darabundit_scavone", "damp_dc") => m("scale on the viscous wall loss", "none", part),
111        ("darabundit_scavone", "damp_freq") => m("scale on the thermal wall loss", "none", part),
112        (_, "damp_dc") => m("frequency-independent loss", "1/s", part),
113        (_, "damp_freq") => m("frequency-dependent loss", "m^2/s", part),
114        (_, "b") => m("string stiffness (inharmonicity)", "none", "string"),
115        (_, "strike_pos") => m(
116            "where the hammer strikes, along the length",
117            "none",
118            "hammer",
119        ),
120        (_, "strike_x") => m("where the hammer strikes, across x", "none", "hammer"),
121        (_, "strike_y") => m("where the hammer strikes, across y", "none", "hammer"),
122        (_, "vel") => m("hammer velocity at contact", "m/s", "hammer"),
123        (_, "hammer_mass") => m("hammer mass", "kg", "hammer"),
124        (_, "hammer_k") => m("felt stiffness K in F = K compression^p", "N/m^p", "hammer"),
125        (_, "hammer_p") => m("felt stiffness exponent p", "none", "hammer"),
126        (_, "unison_count") => m("strings struck together, 1 to 3", "none", "string"),
127        (_, "detune") => m(
128            "frequency ratio across the unison's outer strings",
129            "none",
130            "string",
131        ),
132        (_, "bridge_coupling") => m(
133            "bridge resistance, in string wave impedances",
134            "none",
135            "bridge",
136        ),
137        (_, "bridge_mass") => m("bridge mass; 0 is massless", "kg", "bridge"),
138        (_, "release") => m(
139            "when the felt damper lands on every string; unbound is never",
140            "s",
141            "damper",
142        ),
143        (_, "damper_pos") => m("where the felt presses, along the length", "none", "damper"),
144        (_, "damper_r") => m("felt dashpot once ramped in", "N*s/m", "damper"),
145        (_, "damper_k") => m("felt spring, whole from the landing", "N/m", "damper"),
146        (_, "damper_ramp") => m("time the felt dashpot ramps in over", "s", "damper"),
147        (_, "bow_pos") => m("where the bow touches, along the length", "none", "bow"),
148        (_, "bow_vel") => m("bow velocity", "m/s", "bow"),
149        (_, "bow_force") => m("force pressing the bow on the string", "N", "bow"),
150        (_, "mu_s") => m("static friction coefficient", "none", "friction"),
151        (_, "mu_c") => m("sliding (Coulomb) friction coefficient", "none", "friction"),
152        (_, "stribeck_vel") => m(
153            "slip speed over which static friction falls to sliding",
154            "m/s",
155            "friction",
156        ),
157        (_, "bristle_stiffness") => m("bristle stiffness s0", "N/m", "friction"),
158        (_, "bristle_damping") => m("bristle damping s1", "N*s/m", "friction"),
159        (_, "viscous_friction") => m("viscous friction s2", "N*s/m", "friction"),
160        (_, "radius_in") => m("bore radius at the excited end", "m", "bore"),
161        (_, "radius_out") => m("bore radius at the far end, a cone between", "m", "bore"),
162        (_, "excite_pos") => m("where the pulse enters, along the length", "none", "source"),
163        (_, "pulse_amp") => m("pressure pulse peak", "Pa", "source"),
164        (_, "pulse_width") => m("pressure pulse duration", "s", "source"),
165        (_, "aspect_ratio") => m("side ratio lx/ly at a fixed area", "none", "membrane"),
166        (_, "aspect_y") => m("room depth, in room lengths", "none", "room"),
167        (_, "aspect_z") => m("room height, in room lengths", "none", "room"),
168        (_, "listener_x") => m("listener position along the length", "none", "listener"),
169        (_, "listener_y") => m("listener position along the depth", "none", "listener"),
170        (_, "listener_z") => m("listener position along the height", "none", "listener"),
171        _ => return None,
172    })
173}
174
175/// `string2_cents` is `(2, "_cents")`.
176fn numbered<'k>(key: &'k str, stem: &str) -> Option<(u32, &'k str)> {
177    let rest = key.strip_prefix(stem)?;
178    let digits = rest.find(|c: char| !c.is_ascii_digit())?;
179    Some((rest[..digits].parse().ok()?, &rest[digits..]))
180}
181
182/// A strike's hammer, and the decay every mode shares: `1/tau = damp_dc + damp_freq f^2`.
183fn modal(builtin: &str, key: &str) -> Option<Meaning> {
184    Some(match (builtin, key) {
185        ("hammer_pulse", "vel") => m("strike velocity", "m/s", "hammer"),
186        ("helmholtz", "volume") => m("cavity volume", "m^3", "cavity"),
187        ("string", "f0") => m("fundamental frequency", "Hz", "string"),
188        ("membrane" | "room", "lx") => m("side along x", "m", builtin_part(builtin)),
189        ("membrane" | "room", "ly") => m("side along y", "m", builtin_part(builtin)),
190        ("room", "lz") => m("side along z", "m", "room"),
191        ("bar" | "bore", "length") => m("length", "m", builtin_part(builtin)),
192        (_, "damp_dc") => m("decay rate every mode shares", "1/s", "damping"),
193        (_, "damp_freq") => m("decay rate per squared hertz", "s", "damping"),
194        (_, "modes") => plain("how many modes the bank holds", "none"),
195        (_, "vel") => m(
196            "strike velocity; omitted, an impulse rings the bank",
197            "m/s",
198            "hammer",
199        ),
200        (_, "at") => m("when the strike lands", "s", "hammer"),
201        (_, "contact") => m("contact time at 3.2 m/s", "s", "hammer"),
202        (_, "p") => m("felt stiffness exponent", "none", "hammer"),
203        (_, "force") => m("peak force at 3.2 m/s", "N", "hammer"),
204        ("string", "inharmonicity") => m("string stiffness B", "none", "string"),
205        ("string", "strike") => m("where the strike lands, along the length", "none", "hammer"),
206        ("membrane", "tension") => m("membrane tension", "N/m", "membrane"),
207        ("membrane", "density") => m("membrane areal density", "kg/m^2", "membrane"),
208        ("membrane", "strike_x") => m("where the strike lands, across x", "none", "hammer"),
209        ("membrane", "strike_y") => m("where the strike lands, across y", "none", "hammer"),
210        ("bar", "thickness") => m("bar thickness", "m", "bar"),
211        ("bar", "young") => m("Young's modulus", "Pa", "bar"),
212        ("bar", "density") => m("bar density", "kg/m^3", "bar"),
213        ("bore", "radius") => m("bore radius", "m", "bore"),
214        ("bore" | "room" | "helmholtz", "speed") => m("speed of sound", "m/s", "air"),
215        ("bore", "closed") => m("1 closes one end, 0 leaves both open", "none", "bore"),
216        ("helmholtz", "neck_area") => m("neck cross-section", "m^2", "neck"),
217        ("helmholtz", "neck_length") => m("neck length", "m", "neck"),
218        ("helmholtz", "radius") => m("neck radius, for the 1.7 r end correction", "m", "neck"),
219        _ => return None,
220    })
221}
222
223fn builtin_part(builtin: &str) -> &'static str {
224    match builtin {
225        "membrane" => "membrane",
226        "room" => "room",
227        "bar" => "bar",
228        _ => "bore",
229    }
230}