1use std::collections::{BTreeMap, HashMap};
12use std::sync::OnceLock;
13
14use serde::Deserialize;
15
16use crate::preset::registry::{
17 Build, Law, ParamKind, ParamSpec, Provenance, Registry, UnitKind, UnitLaw,
18};
19use crate::preset::vital::{RETAIL_BUILD, VitalVersion};
20use crate::synth::{Synth, SynthError};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum EngineScale {
27 Indexed,
29 Linear,
31 Quadratic,
33 Cubic,
35 Quartic,
37 SquareRoot,
39 Exponential,
41}
42
43#[derive(Deserialize)]
45#[serde(deny_unknown_fields)]
46struct Source {
47 repository: String,
48 commit: String,
49 licence: String,
50}
51
52#[derive(Deserialize)]
53#[serde(deny_unknown_fields)]
54struct Data {
55 source: Source,
56 groups: Vec<Group>,
57 defaults: BTreeMap<String, f64>,
58}
59
60#[derive(Deserialize)]
61#[serde(deny_unknown_fields)]
62struct Group {
63 prefix: String,
64 label: String,
65 ids: Vec<String>,
66 #[serde(default)]
67 later: Option<Later>,
68 controls: Vec<Control>,
69}
70
71#[derive(Deserialize)]
72#[serde(deny_unknown_fields)]
73struct Later {
74 ids: Vec<String>,
75 since: String,
76}
77
78#[derive(Deserialize)]
79#[serde(deny_unknown_fields)]
80struct Control {
81 name: String,
82 label: String,
83 since: String,
84 kind: ParamKind,
85 min: f64,
86 max: f64,
87 default: f64,
88 scale: EngineScale,
89 #[serde(default)]
90 unit: Option<UnitKind>,
91 #[serde(default)]
92 law: Option<Law>,
93 #[serde(default)]
94 choices: Vec<String>,
95 #[serde(default)]
96 legacy: Legacy,
97}
98
99#[derive(Default, Deserialize)]
100#[serde(untagged)]
101enum Legacy {
102 #[default]
103 None,
104 All(bool),
105 Ids(Vec<String>),
106}
107
108impl Legacy {
109 fn covers(&self, id: &str) -> bool {
110 match self {
111 Self::None => false,
112 Self::All(all) => *all,
113 Self::Ids(ids) => ids.iter().any(|legacy| legacy == id),
114 }
115 }
116}
117
118#[derive(Debug)]
120pub(crate) struct VitalRegistry {
121 pub(crate) registry: Registry,
122 pub(crate) scales: HashMap<String, EngineScale>,
123}
124
125const DATA: &str = include_str!("registry.json");
126
127pub(crate) fn vital_registry() -> Result<&'static VitalRegistry, SynthError> {
129 static REGISTRY: OnceLock<Result<VitalRegistry, String>> = OnceLock::new();
130 REGISTRY
131 .get_or_init(|| build(DATA))
132 .as_ref()
133 .map_err(|cause| SynthError::BrokenData {
134 synth: Synth::Vital,
135 cause: cause.clone(),
136 })
137}
138
139fn build(text: &str) -> Result<VitalRegistry, String> {
140 let data: Data =
141 serde_json::from_str(text).map_err(|error| format!("registry.json: {error}"))?;
142 let mut entries = Vec::new();
143 let mut scales = HashMap::new();
144 for group in &data.groups {
145 for id in &group.ids {
146 let later = group
147 .later
148 .as_ref()
149 .filter(|later| later.ids.contains(id))
150 .map(|later| later.since.as_str());
151 for control in &group.controls {
152 let key = if group.prefix.is_empty() {
153 control.name.clone()
154 } else {
155 format!("{}_{id}_{}", group.prefix, control.name)
156 };
157 let label = if group.prefix.is_empty() {
158 control.label.clone()
159 } else {
160 format!("{} {} {}", group.label, id.to_uppercase(), control.label)
161 };
162 let default = data.defaults.get(&key).copied().unwrap_or(control.default);
163 let legacy = control.legacy.covers(id);
164 let spec = spec(
165 key,
166 label,
167 control,
168 group,
169 later,
170 default,
171 legacy,
172 &data.source,
173 )?;
174 scales.insert(spec.key.clone(), control.scale);
175 entries.push(spec);
176 }
177 }
178 }
179 if let Some(key) = data.defaults.keys().find(|key| !scales.contains_key(*key)) {
180 return Err(format!("default override for unknown key {key:?}"));
181 }
182 let registry = Registry::new(Synth::Vital, entries)?;
183 Ok(VitalRegistry { registry, scales })
184}
185
186#[expect(
187 clippy::too_many_arguments,
188 reason = "one call site passes the parts of one expanded key"
189)]
190fn spec(
191 key: String,
192 label: String,
193 control: &Control,
194 group: &Group,
195 later: Option<&str>,
196 default: f64,
197 legacy: bool,
198 source: &Source,
199) -> Result<ParamSpec, String> {
200 let law = match (control.unit, control.law.clone()) {
201 (Some(unit), Some(law)) => Some(UnitLaw { unit, law }),
202 (None, None) => None,
203 _ => return Err(format!("{key}: unit and law must come together")),
204 };
205 let since = match later {
206 Some(later) => newer(&control.since, later)?,
207 None => control.since.clone(),
208 };
209 let note = tuning_note(&group.prefix, &control.name, control.kind, law.is_some());
210 let mut evidence = format!(
211 "Vital open source 1.0.6 ({} at {}, {}): src/common/synth_parameters.cpp",
212 source.repository, source.commit, source.licence
213 );
214 if let Some(extra) = law.as_ref().and_then(law_evidence) {
215 evidence.push_str("; ");
216 evidence.push_str(extra);
217 }
218 if note.is_some() {
219 evidence.push_str("; the audible effect is not measured");
220 }
221 let displayed = DISPLAYED.contains(&key.as_str());
222 if displayed {
223 evidence.push_str("; the law is confirmed in the value box of Vital ");
224 evidence.push_str(RETAIL_BUILD);
225 }
226 let rendered = RENDERED.contains(&key.as_str());
227 if rendered {
228 evidence.push_str("; the law is confirmed by a render on the ");
229 evidence.push_str(OPEN_ENGINE);
230 }
231 if !legacy {
235 evidence.push_str(if ENGINE_DIFFERS.contains(&key.as_str()) {
236 "; the open engine reports a different range or labels"
237 } else {
238 "; the open engine reports the same range"
239 });
240 }
241 let verified_on = (!legacy).then(|| Build(RETAIL_BUILD.to_owned()));
242 Ok(ParamSpec {
243 key,
244 kind: control.kind,
245 stored_min: control.min,
246 stored_max: control.max,
247 default,
248 law,
249 label,
250 choices: control.choices.clone(),
251 provenance: match (displayed, rendered, note.is_some()) {
252 (true, _, _) | (false, true, false) => Provenance::Measured,
253 (false, _, true) => Provenance::Interim,
254 (false, false, false) => Provenance::FromFacts,
255 },
256 evidence,
257 first_build: Some(Build(since)),
258 verified_on,
259 tuning_note: note.map(str::to_owned),
260 legacy,
261 })
262}
263
264const DISPLAYED: [&str; 4] = [
269 "distortion_drive",
270 "filter_1_cutoff",
271 "osc_1_unison_detune",
272 "portamento_time",
273];
274
275pub const OPEN_ENGINE: &str = "open engine 1.0.x (vita 0.1.0)";
278
279const ENGINE_DIFFERS: [&str; 9] = [
283 "delay_aux_tempo",
286 "delay_tempo",
287 "filter_1_style",
289 "filter_2_style",
290 "filter_fx_style",
291 "osc_1_destination",
293 "osc_2_destination",
294 "osc_3_destination",
295 "sample_destination",
296];
297
298const RENDERED: [&str; 6] = [
302 "osc_1_level",
303 "osc_2_level",
304 "osc_3_level",
305 "osc_1_unison_detune",
306 "osc_2_unison_detune",
307 "osc_3_unison_detune",
308];
309
310fn newer(a: &str, b: &str) -> Result<String, String> {
312 let parse = |text: &str| {
313 text.parse::<VitalVersion>()
314 .map_err(|error| format!("build {text:?}: {error}"))
315 };
316 Ok(if parse(a)? >= parse(b)? { a } else { b }.to_owned())
317}
318
319fn law_evidence(law: &UnitLaw) -> Option<&'static str> {
321 match (&law.law, law.unit) {
322 (Law::Exp2 { .. }, UnitKind::Hz) => Some(
323 "note-number frequency law in src/synthesis/framework/utils.h and the rate law in synth_module.cpp",
324 ),
325 (Law::DbOfPower { .. }, _) => {
326 Some("level squared at audio rate in src/synthesis/producers/synth_oscillator.cpp")
327 }
328 (Law::Power { exponent, .. }, UnitKind::Seconds)
329 if (exponent - 4.0).abs() < f64::EPSILON =>
330 {
331 Some("envelope times in seconds in src/synthesis/modulators/envelope.cpp")
332 }
333 _ => None,
334 }
335}
336
337const RESONANCE: &str = "The audible peak depends on the filter model and is not measured. Raise it while a note holds until the peak near the cutoff matches the stem; back off when it starts to whistle.";
338const DRIVE: &str = "The drive response inside each filter model is not measured. Raise it until the added harmonics match the stem, then check the level.";
339const FILTER_SHAPE: &str = "What this control does depends on the filter model and style. Sweep it slowly while a note holds and stop where the tone matches the stem.";
340const DISTORTION: &str = "The distortion curve is not measured. Raise it from zero until the added harmonics match the stem on held notes.";
341const COMPRESSOR: &str = "The compressor response is not measured. Compare the attack and the held level of the stem with the patch and set it by ear.";
342const EQ: &str = "The EQ shape at this setting is not measured. Sweep it while a note plays and match the brightness of the stem.";
343const FEEDBACK: &str = "How much each repeat or sweep keeps is not measured. Raise it until the tail or the ringing of the effect matches the stem.";
344const DEPTH: &str =
345 "The depth of the sweep is not measured. Raise it until the movement matches the stem.";
346const MIX: &str = "The balance law between the dry and the wet sound is not measured. Raise it until the effect is as loud as in the stem.";
347const REVERB: &str = "The size, tone and motion of the reverb are not measured. Match the length and the colour of the tail by ear.";
348const WARP: &str = "The sound change of this oscillator warp is not measured. Sweep it while a note holds and stop where the timbre matches the stem.";
349const CURVE: &str = "The value bends a curve; negative and positive values bend opposite ways. Match the shape of the stem by ear.";
350const GENERIC: &str = "No physical unit is claimed for this control. Sweep it while a note holds and stop where the sound matches the stem.";
351
352fn tuning_note(prefix: &str, name: &str, kind: ParamKind, has_unit: bool) -> Option<&'static str> {
358 if kind != ParamKind::Float {
359 return None;
360 }
361 let family = match (prefix, name) {
362 (_, name)
363 if name.starts_with("eq_")
364 && (name.ends_with("_gain") || name.ends_with("_resonance")) =>
365 {
366 Some(EQ)
367 }
368 ("filter", "resonance") | ("", "distortion_filter_resonance") => Some(RESONANCE),
369 ("filter", "drive") => Some(DRIVE),
370 ("filter", name) if name.starts_with("blend") || name.starts_with("formant_") => {
371 Some(FILTER_SHAPE)
372 }
373 ("filter", "mix") => Some(MIX),
374 ("", "distortion_drive" | "distortion_mix" | "distortion_filter_blend") => Some(DISTORTION),
375 ("", name) if name.starts_with("compressor_") => Some(COMPRESSOR),
376 (
377 "",
378 "reverb_size"
379 | "reverb_chorus_amount"
380 | "reverb_high_shelf_gain"
381 | "reverb_low_shelf_gain",
382 ) => Some(REVERB),
383 (_, name) if name.ends_with("_feedback") => Some(FEEDBACK),
384 (_, name) if name.ends_with("_mod_depth") => Some(DEPTH),
385 (_, name) if name.ends_with("_dry_wet") => Some(MIX),
386 (
387 "osc",
388 "distortion_amount"
389 | "distortion_spread"
390 | "spectral_morph_amount"
391 | "spectral_morph_spread",
392 ) => Some(WARP),
393 _ => None,
394 };
395 family.or_else(|| match (has_unit, name.ends_with("_power")) {
396 (true, _) => None,
397 (false, true) => Some(CURVE),
398 (false, false) => Some(GENERIC),
399 })
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::preset::registry::{ParamLaw, ParamRegistry, Physical};
406 use approx::assert_relative_eq;
407
408 fn vital() -> &'static VitalRegistry {
409 vital_registry().unwrap()
410 }
411
412 fn spec(key: &str) -> &'static ParamSpec {
413 vital().registry.get(key).unwrap()
414 }
415
416 fn physical(key: &str, stored: f64) -> f64 {
418 spec(key).physical(stored).unwrap().get()
419 }
420
421 #[test]
422 fn the_retail_build_confirms_every_created_key_and_default() {
423 let registry = &vital().registry;
424 let verified = registry
425 .iter()
426 .filter(|spec| {
427 spec.verified_on
428 .as_ref()
429 .is_some_and(|build| build.0 == RETAIL_BUILD)
430 })
431 .count();
432 assert_eq!(verified, 794 - 22);
433 let same = registry
434 .iter()
435 .filter(|spec| {
436 spec.evidence
437 .ends_with("the open engine reports the same range")
438 })
439 .count();
440 assert_eq!(same, 794 - 22 - ENGINE_DIFFERS.len());
441 for key in ENGINE_DIFFERS {
442 assert!(
443 spec(key).evidence.ends_with("a different range or labels"),
444 "{key}"
445 );
446 }
447 for key in RENDERED {
448 assert_eq!(spec(key).provenance, Provenance::Measured, "{key}");
449 assert!(
450 spec(key).evidence.contains("confirmed by a render"),
451 "{key}"
452 );
453 }
454 assert!(spec("sub_on").verified_on.is_none());
455 }
456
457 #[test]
458 fn the_table_has_794_keys_and_22_legacy() {
459 let registry = &vital().registry;
460 assert_eq!(registry.entries().len(), 794);
461 let legacy: Vec<_> = registry
462 .iter()
463 .filter(|spec| spec.legacy)
464 .map(|spec| spec.key.as_str())
465 .collect();
466 assert_eq!(legacy.len(), 22, "{legacy:?}");
467 for key in [
468 "sub_direct_out",
469 "sub_waveform",
470 "filter_1_osc1_input",
471 "filter_fx_sample_input",
472 "filter_fx_filter_input",
473 "compressor_low_band_unused",
474 ] {
475 assert!(spec(key).legacy, "{key}");
476 }
477 assert!(!spec("filter_1_filter_input").legacy);
478 }
479
480 #[test]
481 fn group_and_scale_counts_match_the_source() {
482 let count = |prefix: &str| {
483 vital()
484 .registry
485 .iter()
486 .filter(|spec| spec.key.starts_with(prefix))
487 .count()
488 };
489 assert_eq!(count("env_"), 54);
490 assert_eq!(count("lfo_"), 96);
491 assert_eq!(count("random_"), 32);
492 assert_eq!(count("osc_"), 87);
493 assert_eq!(count("filter_1_") + count("filter_2_"), 40);
494 assert_eq!(count("filter_fx_"), 20);
495 assert_eq!(count("modulation_"), 320);
496 let mut scales: HashMap<EngineScale, usize> = HashMap::new();
497 for scale in vital().scales.values() {
498 *scales.entry(*scale).or_default() += 1;
499 }
500 assert_eq!(scales.get(&EngineScale::Indexed), Some(&370));
501 assert_eq!(scales.get(&EngineScale::Linear), Some(&351));
502 assert_eq!(scales.get(&EngineScale::Quartic), Some(&30));
503 assert_eq!(scales.get(&EngineScale::Exponential), Some(&30));
504 assert_eq!(scales.get(&EngineScale::Quadratic), Some(&12));
505 assert_eq!(scales.get(&EngineScale::SquareRoot), Some(&1));
506 assert_eq!(scales.get(&EngineScale::Cubic), None);
507 }
508
509 #[test]
510 fn later_instances_carry_their_first_build() {
511 let build = |key: &str| spec(key).first_build.clone().unwrap().0;
512 assert_eq!(build("osc_3_level"), "0.5.0");
513 assert_eq!(build("osc_1_level"), "0.0.0");
514 assert_eq!(build("modulation_33_amount"), "0.6.1");
515 assert_eq!(build("modulation_32_amount"), "0.0.0");
516 }
517
518 #[test]
519 fn overridden_defaults_hold() {
520 assert_eq!(spec("osc_1_on").default, 1.0);
521 assert_eq!(spec("osc_2_on").default, 0.0);
522 assert_eq!(spec("osc_2_destination").default, 1.0);
523 assert_eq!(spec("osc_3_destination").default, 3.0);
524 }
525
526 #[test]
530 fn cutoff_follows_the_note_law() {
531 for (stored, hz) in [
533 (8.0, 8.175_798_915_6 * 2f64.powf(8.0 / 12.0)),
534 (36.0, 8.175_798_915_6 * 8.0),
535 (60.0, 8.175_798_915_6 * 32.0),
536 (84.0, 8.175_798_915_6 * 128.0),
537 (100.0, 8.175_798_915_6 * 2f64.powf(100.0 / 12.0)),
538 (136.0, 8.175_798_915_6 * 2f64.powf(136.0 / 12.0)),
539 ] {
540 for key in ["filter_1_cutoff", "filter_2_cutoff", "filter_fx_cutoff"] {
541 assert_relative_eq!(
542 physical(key, stored),
543 hz,
544 epsilon = 0.0,
545 max_relative = 1e-6
546 );
547 }
548 }
549 assert_relative_eq!(physical("filter_1_cutoff", 8.0), 12.98, max_relative = 1e-3);
551 assert_relative_eq!(physical("filter_1_cutoff", 60.0), 261.63, epsilon = 0.005);
552 assert_relative_eq!(
553 physical("filter_1_cutoff", 136.0),
554 21_096.0,
555 max_relative = 1e-3
556 );
557 for key in [
558 "delay_filter_cutoff",
559 "distortion_filter_cutoff",
560 "chorus_cutoff",
561 "phaser_center",
562 "flanger_center",
563 "eq_low_cutoff",
564 "eq_band_cutoff",
565 "eq_high_cutoff",
566 "reverb_pre_low_cutoff",
567 "reverb_high_shelf_cutoff",
568 ] {
569 assert_eq!(spec(key).unit(), Some(UnitKind::Hz), "{key}");
570 assert_relative_eq!(physical(key, 60.0), 261.625_565_3, max_relative = 1e-6);
571 }
572 }
573
574 #[test]
575 fn envelope_times_are_fourth_powers() {
576 assert_relative_eq!(
578 physical("env_1_attack", 2.378_42),
579 2.378_42f64.powi(4),
580 max_relative = 1e-9
581 );
582 assert_relative_eq!(physical("env_3_decay", 2.378_42), 32.0, max_relative = 1e-5);
583 assert_relative_eq!(physical("env_6_release", 1.0), 1.0, max_relative = 1e-12);
584 assert_relative_eq!(
585 physical("env_2_hold", spec("env_2_hold").stored_max),
586 4.0,
587 max_relative = 1e-9
588 );
589 assert_relative_eq!(
590 physical("env_1_delay", std::f64::consts::SQRT_2),
591 4.0,
592 max_relative = 1e-9
593 );
594 }
595
596 #[test]
597 fn rates_and_times_are_powers_of_two() {
598 assert_relative_eq!(
600 physical("lfo_1_frequency", -7.0),
601 0.007_812_5,
602 max_relative = 1e-12
603 );
604 assert_relative_eq!(
605 physical("lfo_8_frequency", 9.0),
606 512.0,
607 max_relative = 1e-12
608 );
609 assert_relative_eq!(
610 physical("random_4_frequency", 1.0),
611 2.0,
612 max_relative = 1e-12
613 );
614 for key in [
615 "delay_frequency",
616 "phaser_frequency",
617 "flanger_frequency",
618 "chorus_frequency",
619 ] {
620 assert_eq!(spec(key).unit(), Some(UnitKind::Hz), "{key}");
621 assert_relative_eq!(physical(key, 0.0), 1.0, max_relative = 1e-12);
622 }
623 assert_relative_eq!(
624 physical("portamento_time", -10.0),
625 2f64.powi(-10),
626 max_relative = 1e-12
627 );
628 assert_relative_eq!(
629 physical("reverb_decay_time", 0.0),
630 1.0,
631 max_relative = 1e-12
632 );
633 assert_eq!(spec("chorus_delay_1").unit(), Some(UnitKind::Seconds));
634 assert_relative_eq!(
635 physical("chorus_delay_1", -9.0),
636 2f64.powi(-9),
637 max_relative = 1e-12
638 );
639 }
640
641 #[test]
642 fn levels_pans_tunes_and_detune_follow_their_laws() {
643 let default = spec("osc_1_level").default;
645 assert_relative_eq!(
646 physical("osc_1_level", default),
647 40.0 * default.log10(),
648 max_relative = 1e-9
649 );
650 assert_relative_eq!(
651 physical("osc_1_level", default),
652 -6.0206,
653 max_relative = 1e-4
654 );
655 assert_relative_eq!(physical("sample_level", 1.0), 0.0, epsilon = 1e-12);
656 assert_relative_eq!(
658 physical("volume", 5_473.040_4),
659 5_473.040_4f64.sqrt() - 80.0,
660 max_relative = 1e-9
661 );
662 assert_relative_eq!(physical("volume", 5_473.040_4), -6.02, max_relative = 1e-3);
663 assert_relative_eq!(physical("volume", 7_399.440_4), 6.02, max_relative = 1e-3);
664 assert_relative_eq!(physical("osc_2_tune", 0.5), 50.0, max_relative = 1e-12);
666 assert_eq!(spec("osc_2_tune").unit(), Some(UnitKind::Cents));
667 assert_relative_eq!(physical("osc_1_pan", -0.5), -50.0, max_relative = 1e-12);
668 assert_eq!(spec("osc_1_pan").unit(), Some(UnitKind::PercentBipolar));
669 assert_relative_eq!(
670 physical("filter_1_keytrack", 1.0),
671 100.0,
672 max_relative = 1e-12
673 );
674 assert_relative_eq!(
676 physical("osc_1_unison_detune", 4.472_135_955),
677 20.0,
678 max_relative = 1e-9
679 );
680 assert_relative_eq!(
681 physical("osc_1_unison_detune", 10.0),
682 100.0,
683 max_relative = 1e-12
684 );
685 assert_eq!(spec("osc_1_transpose").kind, ParamKind::Count);
687 assert_relative_eq!(
688 physical("osc_1_transpose", -48.0),
689 -48.0,
690 max_relative = 1e-12
691 );
692 }
693
694 #[test]
695 fn every_law_round_trips_at_its_range_and_default() {
696 for spec in vital().registry.iter() {
697 let Some(unit_law) = &spec.law else { continue };
698 let middle = f64::midpoint(spec.stored_min, spec.stored_max);
699 for stored in [spec.stored_min, spec.default, middle, spec.stored_max] {
700 let Ok(value) = unit_law.law.to_physical(stored) else {
701 assert_eq!(
703 (stored, &unit_law.law),
704 (0.0, &Law::DbOfPower { exponent: 2.0 }),
705 "{}",
706 spec.key
707 );
708 continue;
709 };
710 let back = unit_law.law.to_stored(value).unwrap();
711 assert!(
712 (back - stored).abs() <= 1e-9 * stored.abs().max(1e-9),
713 "{}: {stored} came back as {back}",
714 spec.key
715 );
716 }
717 }
718 }
719
720 #[test]
721 fn the_value_box_confirms_four_laws() {
722 let seconds = |key: &str, stored: f64| match spec(key).physical(stored).unwrap() {
725 Physical::Seconds(value) => value.get(),
726 other => panic!("{other:?}"),
727 };
728 assert_relative_eq!(seconds("portamento_time", -1.0), 0.5);
729 assert_relative_eq!(
730 seconds("portamento_time", -1.88),
731 0.271_684,
732 max_relative = 1e-5
733 );
734 let percent = |stored: f64| match spec("osc_1_unison_detune").physical(stored).unwrap() {
735 Physical::PercentUnipolar(value) => value.get(),
736 other => panic!("{other:?}"),
737 };
738 assert_relative_eq!(percent(5.0), 25.0);
739 assert_relative_eq!(percent(6.05), 36.60, max_relative = 1e-3);
740 let hz = |stored: f64| match spec("filter_1_cutoff").physical(stored).unwrap() {
742 Physical::Hz(value) => value.get(),
743 other => panic!("{other:?}"),
744 };
745 for (shown, stored) in [(12.0, 72.0), (6.860_02, 66.86)] {
746 let note: f64 = 60.0 + shown;
747 assert_relative_eq!(
748 hz(stored),
749 440.0 * ((note - 69.0) / 12.0).exp2(),
750 max_relative = 1e-5
751 );
752 }
753 let db = |stored: f64| match spec("distortion_drive").physical(stored).unwrap() {
754 Physical::Decibels(value) => value.get(),
755 other => panic!("{other:?}"),
756 };
757 assert_relative_eq!(db(12.0), 12.0);
758 assert_relative_eq!(db(20.507), 20.5071, max_relative = 1e-5);
759 for key in DISPLAYED {
760 assert_eq!(spec(key).provenance, Provenance::Measured, "{key}");
761 assert_eq!(spec(key).verified_on, Some(Build(RETAIL_BUILD.to_owned())));
762 }
763 assert!(spec("distortion_drive").tuning_note.is_some());
764 }
765
766 #[test]
767 fn interim_entries_are_listed_with_notes() {
768 let interim: Vec<_> = vital()
769 .registry
770 .iter()
771 .filter(|spec| spec.provenance == Provenance::Interim)
772 .collect();
773 for key in [
774 "filter_1_resonance",
775 "filter_2_resonance",
776 "filter_fx_resonance",
777 ] {
778 assert!(interim.iter().any(|spec| spec.key == key), "{key}");
779 assert_eq!(spec(key).tuning_note.as_deref(), Some(RESONANCE));
780 }
781 for spec in &interim {
782 assert!(spec.tuning_note.is_some(), "{}", spec.key);
783 }
784 for key in [
785 "filter_1_drive",
786 "eq_low_gain",
787 "compressor_band_gain",
788 "delay_feedback",
789 "reverb_size",
790 "env_1_sustain",
791 ] {
792 assert_eq!(spec(key).provenance, Provenance::Interim, "{key}");
793 }
794 for key in [
795 "filter_2_cutoff",
796 "env_1_attack",
797 "lfo_1_frequency",
798 "volume",
799 "osc_1_tune",
800 "filter_1_model",
801 "osc_1_unison_voices",
802 ] {
803 assert_eq!(spec(key).provenance, Provenance::FromFacts, "{key}");
804 }
805 }
806
807 #[test]
808 fn choices_cover_their_range() {
809 assert_eq!(spec("filter_1_model").choices.len(), 8);
810 assert_eq!(spec("filter_1_model").choices[0], "Analog");
811 assert_eq!(spec("filter_1_on").kind, ParamKind::Flag);
812 let tempo = spec("delay_tempo");
813 assert_eq!(tempo.kind, ParamKind::Choice);
814 assert_eq!(tempo.stored_min, 4.0);
815 assert_eq!(tempo.choices.first().map(String::as_str), Some("4/1"));
816 }
817
818 #[test]
819 fn hz_edits_reach_stored_cutoffs_exactly() {
820 let cutoff = spec("filter_1_cutoff");
821 let stored = cutoff
822 .stored(&Physical::parse(UnitKind::Hz, "1046.502261 Hz").unwrap())
823 .unwrap();
824 assert_relative_eq!(stored, 84.0, max_relative = 1e-8);
825 let message = cutoff
826 .stored(&Physical::parse(UnitKind::Hz, "5 Hz").unwrap())
827 .unwrap_err()
828 .to_string();
829 assert!(
830 message.starts_with("filter_1_cutoff: 5 Hz is outside 12.97"),
831 "{message}"
832 );
833 }
834
835 #[test]
836 fn broken_data_is_an_error() {
837 assert!(build("{}").is_err());
838 assert!(build(r#"{"source":{"repository":"r","commit":"c","licence":"l"},"groups":[],"defaults":{"nope":1.0}}"#).is_err());
839 }
840
841 #[test]
842 fn newer_compares_builds() {
843 assert_eq!(newer("0.0.0", "0.5.0").unwrap(), "0.5.0");
844 assert_eq!(newer("0.7.4", "0.5.0").unwrap(), "0.7.4");
845 assert!(newer("x", "0.5.0").is_err());
846 }
847}