Skip to main content

sound_to_patch/preset/vital/
registry.rs

1//! The Vital parameter registry, built from `registry.json`.
2//!
3//! The data file holds the control groups as facts in our own layout: one
4//! row per control template with its range, default, engine scale, unit and
5//! law, and the instance ids of each group. `cargo xtask vital-registry`
6//! regenerates it from a checkout of the open source. This module expands
7//! the groups to 794 keys and adds what is our judgment, not a fact: the
8//! provenance of each law and the tuning notes. The file names the source
9//! repository, its commit and its licence.
10
11use 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/// The engine transform of a stored value, applied after modulation adds
23/// to it.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum EngineScale {
27    /// Integer steps: menus, switches, counts.
28    Indexed,
29    /// `x`.
30    Linear,
31    /// `x^2`.
32    Quadratic,
33    /// `x^3`.
34    Cubic,
35    /// `x^4`.
36    Quartic,
37    /// `sqrt(x)`, plus an offset.
38    SquareRoot,
39    /// `2^x`.
40    Exponential,
41}
42
43/// Where the extracted facts come from.
44#[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/// The registry with the engine scale of each key.
119#[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
127/// The Vital registry, built once.
128pub(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    // Retail 1.0.7 saved every created control at its default. That
232    // confirms the key and the default only; the range is the open
233    // engine's, and the law is the source's unless the evidence says more.
234    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
264/// Controls whose law the value box of retail Vital confirmed: a typed
265/// value was stored as the law gives, and a stored value read back as the
266/// law gives. The drive keeps its tuning note: its law is in decibels, but
267/// the sound of the distortion curve is not measured.
268const DISPLAYED: [&str; 4] = [
269    "distortion_drive",
270    "filter_1_cutoff",
271    "osc_1_unison_detune",
272    "portamento_time",
273];
274
275/// The engine the render probes of #179 ran on: the open 1.0.x engine
276/// through the `vita` 0.1.0 binding.
277pub const OPEN_ENGINE: &str = "open engine 1.0.x (vita 0.1.0)";
278
279/// Keys whose range or menu labels the open engine reports differently
280/// from the source table. Their ranges stay unconfirmed until a retail
281/// check.
282const ENGINE_DIFFERS: [&str; 9] = [
283    // The binding counts the labels from the range start; the app's
284    // slider indexes them by the value itself.
285    "delay_aux_tempo",
286    "delay_tempo",
287    // The table holds 10 values and 5 labels.
288    "filter_1_style",
289    "filter_2_style",
290    "filter_fx_style",
291    // The table allows 14, which has no label; the engine reports 13.
292    "osc_1_destination",
293    "osc_2_destination",
294    "osc_3_destination",
295    "sample_destination",
296];
297
298/// Controls whose law a render on the open engine confirmed: the level is
299/// the square of the stored value, and the outer unison offset is
300/// `detune_range * detune^2` cents.
301const 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
310/// The later of two build texts.
311fn 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
319/// The source files behind the laws that are not read off the table.
320fn 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
352/// The tuning note of an interim control, or `None` for a law from facts.
353///
354/// Menus, switches and counts are facts. A real-valued control is interim
355/// when it claims no unit, or when its unit is a display scale whose
356/// audible effect no source states.
357fn 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    /// The physical value of a stored value, as a plain number.
417    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    // Golden values: each expected number is computed from the formula
527    // beside it, as the facts state the law.
528
529    #[test]
530    fn cutoff_follows_the_note_law() {
531        // Hz = 8.1757989156 * 2^(stored / 12)
532        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        // The facts round these to 12.98, 65.4, 261.63, 1046.5, 2637, 21.1k.
550        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        // seconds = stored^4
577        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        // Hz = 2^stored; seconds = 2^stored
599        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        // dB = 40 * log10(stored)
644        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        // dB = sqrt(stored) - 80
657        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        // cents = 100 * stored; percent = 100 * stored
665        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        // percent = stored^2
675        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        // semitones = stored, whole steps
686        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                    // Only a zero gain has no decibel value.
702                    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        // Pairs from a file saved by Vital 1.0.7: a typed value and the
723        // value it stored, then a stored value and what the box read.
724        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        // The box shows the cutoff as semitones above MIDI note 60.
741        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}