Skip to main content

sound_to_patch/preset/
registry.rs

1//! The parameter registry of a synth: the key, range, unit and law of
2//! every control, with the provenance of each law.
3//!
4//! A [`ParamSpec`] holds one control. Its [`Law`] converts between the
5//! stored value in the preset file and a physical value in an E1 unit.
6//! Laws never round: a non-integer stored value for a menu or a count is an
7//! error, and choosing the nearest option belongs to the mapper.
8//!
9//! Each law carries a [`Provenance`]: read from facts, measured on a render
10//! or probe, or interim. A [`CalibrationSet`] replaces laws with measured
11//! tables without a code change.
12//!
13//! ```
14//! use sound_to_patch::preset::registry::{registry_for, Physical, UnitKind};
15//! use sound_to_patch::synth::Synth;
16//!
17//! let registry = registry_for(Synth::Vital)?;
18//! let cutoff = registry.get("filter_1_cutoff").ok_or("no cutoff")?;
19//! let stored = cutoff.stored(&Physical::parse(UnitKind::Hz, "261.6255653 Hz")?)?;
20//! assert!((stored - 60.0).abs() < 1e-6);
21//! # Ok::<(), Box<dyn std::error::Error>>(())
22//! ```
23
24use std::collections::HashMap;
25use std::error::Error;
26use std::fmt;
27use std::sync::Arc;
28
29use serde::{Deserialize, Serialize};
30
31use crate::synth::{Synth, SynthError};
32use crate::units::{
33    Bipolar, Cents, Decibels, Hz, MidiPitch, Normalized, Octaves, ParseUnitError, Percent, Range,
34    RangeError, Seconds, Semitones, Unipolar,
35};
36
37/// What a stored value is.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ParamKind {
41    /// A real number.
42    Float,
43    /// An integer.
44    Count,
45    /// 0 or 1.
46    Flag,
47    /// An index into the menu labels of the entry.
48    Choice,
49}
50
51/// The E1 unit type of a physical value.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum UnitKind {
55    /// [`Hz`].
56    Hz,
57    /// [`Seconds`].
58    Seconds,
59    /// [`Semitones`].
60    Semitones,
61    /// [`Cents`].
62    Cents,
63    /// [`Octaves`].
64    Octaves,
65    /// [`Decibels`].
66    Decibels,
67    /// [`Normalized`].
68    Normalized,
69    /// [`MidiPitch`].
70    MidiPitch,
71    /// [`Percent<Unipolar>`].
72    PercentUnipolar,
73    /// [`Percent<Bipolar>`].
74    PercentBipolar,
75}
76
77impl UnitKind {
78    /// The name of the unit type, as the unit-string parser names it.
79    #[must_use]
80    pub fn type_name(self) -> &'static str {
81        match self {
82            Self::Hz => "Hz",
83            Self::Seconds => "Seconds",
84            Self::Semitones => "Semitones",
85            Self::Cents => "Cents",
86            Self::Octaves => "Octaves",
87            Self::Decibels => "Decibels",
88            Self::Normalized => "Normalized",
89            Self::MidiPitch => "MidiPitch",
90            Self::PercentUnipolar => "Percent (0 to 100)",
91            Self::PercentBipolar => "Percent (-100 to 100)",
92        }
93    }
94}
95
96/// A physical value in one of the E1 unit types.
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum Physical {
99    /// A frequency.
100    Hz(Hz),
101    /// A time.
102    Seconds(Seconds),
103    /// An interval in semitones.
104    Semitones(Semitones),
105    /// An interval in cents.
106    Cents(Cents),
107    /// An interval in octaves.
108    Octaves(Octaves),
109    /// A level.
110    Decibels(Decibels),
111    /// A knob position.
112    Normalized(Normalized),
113    /// A MIDI pitch.
114    MidiPitch(MidiPitch),
115    /// A percentage from 0 to 100.
116    PercentUnipolar(Percent<Unipolar>),
117    /// A percentage from -100 to 100.
118    PercentBipolar(Percent<Bipolar>),
119}
120
121impl Physical {
122    /// Builds a value of the unit type `kind` from its base number, as
123    /// `get()` returns it: hertz, seconds, percent from 0 to 100, and so on.
124    ///
125    /// # Errors
126    ///
127    /// The [`RangeError`] of the unit type.
128    pub fn new(kind: UnitKind, value: f64) -> Result<Self, RangeError> {
129        Ok(match kind {
130            UnitKind::Hz => Self::Hz(Hz::new(value)?),
131            UnitKind::Seconds => Self::Seconds(Seconds::new(value)?),
132            UnitKind::Semitones => Self::Semitones(Semitones::new(value)?),
133            UnitKind::Cents => Self::Cents(Cents::new(value)?),
134            UnitKind::Octaves => Self::Octaves(Octaves::new(value)?),
135            UnitKind::Decibels => Self::Decibels(Decibels::new(value)?),
136            UnitKind::Normalized => Self::Normalized(Normalized::new(value)?),
137            UnitKind::MidiPitch => Self::MidiPitch(MidiPitch::new(value)?),
138            UnitKind::PercentUnipolar => Self::PercentUnipolar(Percent::new(value)?),
139            UnitKind::PercentBipolar => Self::PercentBipolar(Percent::new(value)?),
140        })
141    }
142
143    /// Parses a unit string such as `"1.2 kHz"` into the unit type `kind`.
144    ///
145    /// The message of the error names the type, not the parameter. The
146    /// caller adds the parameter name.
147    ///
148    /// # Errors
149    ///
150    /// The [`ParseUnitError`] of the unit-string parser.
151    pub fn parse(kind: UnitKind, text: &str) -> Result<Self, ParseUnitError> {
152        Ok(match kind {
153            UnitKind::Hz => Self::Hz(text.parse()?),
154            UnitKind::Seconds => Self::Seconds(text.parse()?),
155            UnitKind::Semitones => Self::Semitones(text.parse()?),
156            UnitKind::Cents => Self::Cents(text.parse()?),
157            UnitKind::Octaves => Self::Octaves(text.parse()?),
158            UnitKind::Decibels => Self::Decibels(text.parse()?),
159            UnitKind::Normalized => {
160                return Err(ParseUnitError::no_unit_string(text, "Normalized"));
161            }
162            UnitKind::MidiPitch => Self::MidiPitch(text.parse()?),
163            UnitKind::PercentUnipolar => Self::PercentUnipolar(text.parse()?),
164            UnitKind::PercentBipolar => Self::PercentBipolar(text.parse()?),
165        })
166    }
167
168    /// The unit type of the value.
169    #[must_use]
170    pub fn kind(self) -> UnitKind {
171        match self {
172            Self::Hz(_) => UnitKind::Hz,
173            Self::Seconds(_) => UnitKind::Seconds,
174            Self::Semitones(_) => UnitKind::Semitones,
175            Self::Cents(_) => UnitKind::Cents,
176            Self::Octaves(_) => UnitKind::Octaves,
177            Self::Decibels(_) => UnitKind::Decibels,
178            Self::Normalized(_) => UnitKind::Normalized,
179            Self::MidiPitch(_) => UnitKind::MidiPitch,
180            Self::PercentUnipolar(_) => UnitKind::PercentUnipolar,
181            Self::PercentBipolar(_) => UnitKind::PercentBipolar,
182        }
183    }
184
185    /// The base number of the value, as the unit type's `get()` returns it.
186    #[must_use]
187    pub fn get(self) -> f64 {
188        match self {
189            Self::Hz(value) => value.get(),
190            Self::Seconds(value) => value.get(),
191            Self::Semitones(value) => value.get(),
192            Self::Cents(value) => value.get(),
193            Self::Octaves(value) => value.get(),
194            Self::Decibels(value) => value.get(),
195            Self::Normalized(value) => value.get(),
196            Self::MidiPitch(value) => value.get(),
197            Self::PercentUnipolar(value) => value.get(),
198            Self::PercentBipolar(value) => value.get(),
199        }
200    }
201}
202
203impl fmt::Display for Physical {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            Self::Hz(value) => value.fmt(f),
207            Self::Seconds(value) => value.fmt(f),
208            Self::Semitones(value) => value.fmt(f),
209            Self::Cents(value) => value.fmt(f),
210            Self::Octaves(value) => value.fmt(f),
211            Self::Decibels(value) => value.fmt(f),
212            Self::Normalized(value) => write!(f, "{}", value.get()),
213            Self::MidiPitch(value) => value.fmt(f),
214            Self::PercentUnipolar(value) => value.fmt(f),
215            Self::PercentBipolar(value) => value.fmt(f),
216        }
217    }
218}
219
220/// A law between the stored value of a control and its physical value.
221///
222/// Both directions use the base number of the unit type: hertz, seconds,
223/// percent from 0 to 100, decibels. A law is strictly monotone over its
224/// domain and never rounds.
225pub trait ParamLaw: fmt::Debug + Send + Sync {
226    /// The physical value of a stored value.
227    ///
228    /// # Errors
229    ///
230    /// A [`RangeError`] when the stored value is outside the domain of the
231    /// law or the result is not finite.
232    fn to_physical(&self, stored: f64) -> Result<f64, RangeError>;
233
234    /// The stored value of a physical value.
235    ///
236    /// # Errors
237    ///
238    /// A [`RangeError`] when the physical value has no stored value under
239    /// the law or the result is not finite.
240    fn to_stored(&self, physical: f64) -> Result<f64, RangeError>;
241}
242
243/// The closed-form laws, a measured table and a law an adapter defines.
244#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum Law {
247    /// `scale * s + offset`, for any `s`.
248    Affine {
249        /// Multiplier, not zero.
250        scale: f64,
251        /// Added after the multiplier.
252        offset: f64,
253    },
254    /// `scale * s^exponent + offset`, for `s >= 0`.
255    Power {
256        /// Positive exponent.
257        exponent: f64,
258        /// Multiplier, not zero.
259        scale: f64,
260        /// Added after the multiplier.
261        offset: f64,
262    },
263    /// `scale * 2^(rate * s)`, for any `s`.
264    Exp2 {
265        /// Octaves per stored unit, not zero.
266        rate: f64,
267        /// The value at `s = 0`, positive.
268        scale: f64,
269    },
270    /// `20 * exponent * log10(s)` decibels, for `s > 0`: a stored gain
271    /// raised to `exponent` before it reaches the audio.
272    DbOfPower {
273        /// Positive exponent.
274        exponent: f64,
275    },
276    /// A measured table.
277    Table(Table),
278    /// A law the closed forms do not cover.
279    #[serde(skip)]
280    Custom(CustomLaw),
281}
282
283/// A law an adapter implements itself, shared between registries.
284#[derive(Clone, Debug)]
285pub struct CustomLaw(pub Arc<dyn ParamLaw>);
286
287impl PartialEq for CustomLaw {
288    fn eq(&self, other: &Self) -> bool {
289        Arc::ptr_eq(&self.0, &other.0)
290    }
291}
292
293/// The checked result of a law step, named for the error message.
294fn finite(what: &'static str, value: f64) -> Result<f64, RangeError> {
295    Range::Finite.check(what, value)
296}
297
298impl ParamLaw for Law {
299    fn to_physical(&self, stored: f64) -> Result<f64, RangeError> {
300        let stored = finite("stored value", stored)?;
301        match self {
302            Self::Affine { scale, offset } => finite("Affine law result", scale * stored + offset),
303            Self::Power {
304                exponent,
305                scale,
306                offset,
307            } => {
308                let base = Range::AtLeast(0.0).check("Power law input", stored)?;
309                finite("Power law result", scale * base.powf(*exponent) + offset)
310            }
311            Self::Exp2 { rate, scale } => finite("Exp2 law result", scale * (rate * stored).exp2()),
312            Self::DbOfPower { exponent } => {
313                let gain = Range::Above(0.0).check("DbOfPower law input", stored)?;
314                finite("DbOfPower law result", 20.0 * exponent * gain.log10())
315            }
316            Self::Table(table) => table.to_physical(stored),
317            Self::Custom(law) => law.0.to_physical(stored),
318        }
319    }
320
321    fn to_stored(&self, physical: f64) -> Result<f64, RangeError> {
322        let physical = finite("physical value", physical)?;
323        match self {
324            Self::Affine { scale, offset } => {
325                finite("inverse Affine law result", (physical - offset) / scale)
326            }
327            Self::Power {
328                exponent,
329                scale,
330                offset,
331            } => {
332                let base = Range::AtLeast(0.0)
333                    .check("inverse Power law input", (physical - offset) / scale)?;
334                finite("inverse Power law result", base.powf(exponent.recip()))
335            }
336            Self::Exp2 { rate, scale } => {
337                let ratio = Range::Above(0.0).check("inverse Exp2 law input", physical / scale)?;
338                finite("inverse Exp2 law result", ratio.log2() / rate)
339            }
340            Self::DbOfPower { exponent } => finite(
341                "inverse DbOfPower law result",
342                10f64.powf(physical / (20.0 * exponent)),
343            ),
344            Self::Table(table) => table.to_stored(physical),
345            Self::Custom(law) => law.0.to_stored(physical),
346        }
347    }
348}
349
350/// How a [`Table`] interpolates between its points.
351#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum Interp {
354    /// Linear in the physical value. Suits decibels and percent.
355    Linear,
356    /// Linear in the logarithm of the physical value. Suits hertz and
357    /// seconds; every physical value must be positive.
358    LogPhysical,
359}
360
361/// A piecewise law through measured points. It never extrapolates.
362#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
363#[serde(try_from = "TableData", into = "TableData")]
364pub struct Table {
365    points: Vec<(f64, f64)>,
366    interp: Interp,
367}
368
369#[derive(Serialize, Deserialize)]
370struct TableData {
371    points: Vec<(f64, f64)>,
372    interp: Interp,
373}
374
375impl TryFrom<TableData> for Table {
376    type Error = String;
377
378    fn try_from(data: TableData) -> Result<Self, String> {
379        Self::new(data.points, data.interp)
380    }
381}
382
383impl From<Table> for TableData {
384    fn from(table: Table) -> Self {
385        Self {
386            points: table.points,
387            interp: table.interp,
388        }
389    }
390}
391
392impl Table {
393    /// Builds a table from `(stored, physical)` points.
394    ///
395    /// # Errors
396    ///
397    /// A message when there are fewer than two points, a value is not
398    /// finite, the stored values do not strictly increase, the physical
399    /// values are not strictly monotone, or a physical value is not
400    /// positive under [`Interp::LogPhysical`].
401    pub fn new(points: Vec<(f64, f64)>, interp: Interp) -> Result<Self, String> {
402        if points.len() < 2 {
403            return Err(format!(
404                "a table needs at least two points, got {}",
405                points.len()
406            ));
407        }
408        if let Some((stored, physical)) = points
409            .iter()
410            .find(|(stored, physical)| !stored.is_finite() || !physical.is_finite())
411        {
412            return Err(format!("point ({stored}, {physical}) is not finite"));
413        }
414        if interp == Interp::LogPhysical
415            && let Some((_, physical)) = points.iter().find(|(_, physical)| *physical <= 0.0)
416        {
417            return Err(format!(
418                "physical value {physical} is not positive, which log_physical needs"
419            ));
420        }
421        let rising = points[1].1 > points[0].1;
422        for pair in points.windows(2) {
423            let ((s0, p0), (s1, p1)) = (pair[0], pair[1]);
424            if s1 <= s0 {
425                return Err(format!(
426                    "stored values must strictly increase, but {s1} follows {s0}"
427                ));
428            }
429            let monotone = if rising { p1 > p0 } else { p1 < p0 };
430            if !monotone {
431                return Err(format!(
432                    "physical values must be strictly monotone, but {p1} follows {p0}"
433                ));
434            }
435        }
436        Ok(Self { points, interp })
437    }
438
439    /// The `(stored, physical)` points.
440    #[must_use]
441    pub fn points(&self) -> &[(f64, f64)] {
442        &self.points
443    }
444
445    /// The interpolation.
446    #[must_use]
447    pub fn interp(&self) -> Interp {
448        self.interp
449    }
450
451    fn first(&self) -> (f64, f64) {
452        self.points[0]
453    }
454
455    fn last(&self) -> (f64, f64) {
456        self.points[self.points.len() - 1]
457    }
458
459    fn warp(&self, physical: f64) -> f64 {
460        match self.interp {
461            Interp::Linear => physical,
462            Interp::LogPhysical => physical.ln(),
463        }
464    }
465
466    fn unwarp(&self, value: f64) -> f64 {
467        match self.interp {
468            Interp::Linear => value,
469            Interp::LogPhysical => value.exp(),
470        }
471    }
472
473    fn to_physical(&self, stored: f64) -> Result<f64, RangeError> {
474        let range = Range::Closed(self.first().0, self.last().0);
475        let stored = range.check("Table law input", stored)?;
476        let index = self
477            .points
478            .windows(2)
479            .position(|pair| stored <= pair[1].0)
480            .unwrap_or(self.points.len() - 2);
481        let ((s0, p0), (s1, p1)) = (self.points[index], self.points[index + 1]);
482        let t = (stored - s0) / (s1 - s0);
483        let (w0, w1) = (self.warp(p0), self.warp(p1));
484        finite("Table law result", self.unwarp(w0 + t * (w1 - w0)))
485    }
486
487    fn to_stored(&self, physical: f64) -> Result<f64, RangeError> {
488        let (low, high) = {
489            let (a, b) = (self.first().1, self.last().1);
490            (a.min(b), a.max(b))
491        };
492        let physical = Range::Closed(low, high).check("inverse Table law input", physical)?;
493        let index = self
494            .points
495            .windows(2)
496            .position(|pair| {
497                let (a, b) = (pair[0].1, pair[1].1);
498                physical >= a.min(b) && physical <= a.max(b)
499            })
500            .unwrap_or(self.points.len() - 2);
501        let ((s0, p0), (s1, p1)) = (self.points[index], self.points[index + 1]);
502        let (w0, w1) = (self.warp(p0), self.warp(p1));
503        let t = (self.warp(physical) - w0) / (w1 - w0);
504        finite("inverse Table law result", s0 + t * (s1 - s0))
505    }
506}
507
508/// Where the law of an entry comes from.
509#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
510#[serde(rename_all = "snake_case")]
511pub enum Provenance {
512    /// Read from a cited source, not measured. Needs `evidence`.
513    FromFacts,
514    /// Fit to a render, a probe or a load check. Needs `evidence` and
515    /// `verified_on`.
516    Measured,
517    /// An estimate, or the audible effect is unmeasured. Needs `evidence`
518    /// and a `tuning_note`; an edit of it is marked uncalibrated.
519    Interim,
520}
521
522/// A synth build, such as `1.5.5`. Each adapter defines what its text
523/// holds.
524#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
525#[serde(transparent)]
526pub struct Build(pub String);
527
528impl fmt::Display for Build {
529    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530        f.pad(&self.0)
531    }
532}
533
534/// A unit and the law that converts stored values to it.
535#[derive(Clone, Debug, PartialEq)]
536pub struct UnitLaw {
537    /// The unit type of the physical value.
538    pub unit: UnitKind,
539    /// The law from the stored value.
540    pub law: Law,
541}
542
543/// One control of a synth.
544#[derive(Clone, Debug, PartialEq)]
545pub struct ParamSpec {
546    /// The key in the preset file. Unique within a registry.
547    pub key: String,
548    /// What the stored value is.
549    pub kind: ParamKind,
550    /// The smallest stored value.
551    pub stored_min: f64,
552    /// The largest stored value.
553    pub stored_max: f64,
554    /// The stored value of a new patch, inside the range.
555    pub default: f64,
556    /// The physical unit and its law, or `None` when no unit is claimed.
557    pub law: Option<UnitLaw>,
558    /// The on-screen name of the control.
559    pub label: String,
560    /// Menu labels of a [`ParamKind::Choice`]: `choices[i]` names the
561    /// stored value `stored_min + i`.
562    pub choices: Vec<String>,
563    /// Where the law comes from.
564    pub provenance: Provenance,
565    /// The source file, document or probe behind the entry.
566    pub evidence: String,
567    /// The first build that has the control.
568    pub first_build: Option<Build>,
569    /// The build where a load check or a probe confirmed the key and its
570    /// default. A confirmed range or law is named in `evidence`.
571    pub verified_on: Option<Build>,
572    /// How to tune an interim control by ear and what to hear.
573    pub tuning_note: Option<String>,
574    /// The control is in the table, but no module creates it. Readers
575    /// accept it and writers refuse it.
576    pub legacy: bool,
577}
578
579/// A conversion of one control that failed.
580///
581/// The message names the key, the cause and the fix.
582#[derive(Debug)]
583pub struct ParamError {
584    key: String,
585    cause: String,
586    fix: String,
587    source: Option<Box<dyn Error + Send + Sync>>,
588}
589
590impl ParamError {
591    fn new(key: &str, cause: String, fix: String) -> Self {
592        Self {
593            key: key.to_owned(),
594            cause,
595            fix,
596            source: None,
597        }
598    }
599
600    fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
601        self.source = Some(Box::new(source));
602        self
603    }
604
605    /// The key of the control.
606    #[must_use]
607    pub fn key(&self) -> &str {
608        &self.key
609    }
610}
611
612impl fmt::Display for ParamError {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        write!(f, "{}: {}. {}", self.key, self.cause, self.fix)
615    }
616}
617
618impl Error for ParamError {
619    fn source(&self) -> Option<&(dyn Error + 'static)> {
620        self.source
621            .as_deref()
622            .map(|source| source as &(dyn Error + 'static))
623    }
624}
625
626impl ParamSpec {
627    /// The physical unit, if the entry claims one.
628    #[must_use]
629    pub fn unit(&self) -> Option<UnitKind> {
630        self.law.as_ref().map(|law| law.unit)
631    }
632
633    /// The stored range as text, such as `8.0 to 136.0`.
634    #[must_use]
635    pub fn range_text(&self) -> String {
636        format!("{:?} to {:?}", self.stored_min, self.stored_max)
637    }
638
639    /// Whether a stored value lies inside the range and suits the kind.
640    #[must_use]
641    pub fn accepts(&self, stored: f64) -> bool {
642        stored.is_finite()
643            && (self.stored_min..=self.stored_max).contains(&stored)
644            && (self.kind == ParamKind::Float || stored.fract() == 0.0)
645    }
646
647    /// Whether a saved preset may hold `stored`: [`Self::accepts`], with
648    /// each bound widened to its 32-bit float form, which a synth that
649    /// keeps controls as `f32` saves.
650    #[must_use]
651    pub fn accepts_saved(&self, stored: f64) -> bool {
652        let narrow = |bound: f64| f64::from(crate::cast::f64_to_f32(bound));
653        let low = self.stored_min.min(narrow(self.stored_min));
654        let high = self.stored_max.max(narrow(self.stored_max));
655        stored.is_finite()
656            && (low..=high).contains(&stored)
657            && (self.kind == ParamKind::Float || stored.fract() == 0.0)
658    }
659
660    fn check_stored(&self, stored: f64) -> Result<f64, ParamError> {
661        if !stored.is_finite() || !(self.stored_min..=self.stored_max).contains(&stored) {
662            return Err(ParamError::new(
663                &self.key,
664                format!("stored value {stored:?} is outside {}", self.range_text()),
665                format!("Use a stored value from {}", self.range_text()),
666            ));
667        }
668        if self.kind != ParamKind::Float && stored.fract() != 0.0 {
669            return Err(ParamError::new(
670                &self.key,
671                format!("stored value {stored:?} is not a whole number"),
672                "Pick one whole step; the registry never rounds".to_owned(),
673            ));
674        }
675        Ok(stored)
676    }
677
678    fn unit_law(&self) -> Result<&UnitLaw, ParamError> {
679        self.law.as_ref().ok_or_else(|| {
680            ParamError::new(
681                &self.key,
682                "the control has no physical unit".to_owned(),
683                "Write the stored value instead".to_owned(),
684            )
685        })
686    }
687
688    /// The physical value of a stored value.
689    ///
690    /// # Errors
691    ///
692    /// A [`ParamError`] naming the key when the entry has no unit, the
693    /// stored value is outside the range or not whole where it must be,
694    /// or the law or the unit type rejects it.
695    pub fn physical(&self, stored: f64) -> Result<Physical, ParamError> {
696        let unit_law = self.unit_law()?;
697        let stored = self.check_stored(stored)?;
698        let value = unit_law.law.to_physical(stored).map_err(|error| {
699            ParamError::new(
700                &self.key,
701                format!("stored value {stored:?} has no physical value: {error}"),
702                format!("Use a stored value from {}", self.range_text()),
703            )
704            .with_source(error)
705        })?;
706        Physical::new(unit_law.unit, value).map_err(|error| {
707            ParamError::new(
708                &self.key,
709                format!(
710                    "the law gives {value:?}, which {} rejects: {error}",
711                    unit_law.unit.type_name()
712                ),
713                "Report this; the law and the unit disagree".to_owned(),
714            )
715            .with_source(error)
716        })
717    }
718
719    /// The stored value of a physical value.
720    ///
721    /// # Errors
722    ///
723    /// A [`ParamError`] naming the key when the unit type differs from the
724    /// entry's, the law rejects the value, or the result is outside the
725    /// stored range or not whole where it must be.
726    pub fn stored(&self, value: &Physical) -> Result<f64, ParamError> {
727        let unit_law = self.unit_law()?;
728        if value.kind() != unit_law.unit {
729            return Err(ParamError::new(
730                &self.key,
731                format!(
732                    "{value} is a {}, but the control takes {}",
733                    value.kind().type_name(),
734                    unit_law.unit.type_name()
735                ),
736                format!("Write the value in {}", unit_law.unit.type_name()),
737            ));
738        }
739        let limits = self.physical_limits();
740        let nearest = |toward_max: bool| {
741            let bound = if toward_max {
742                self.stored_max
743            } else {
744                self.stored_min
745            };
746            self.physical(bound)
747                .map_or_else(|_| format!("stored {bound:?}"), |value| value.to_string())
748        };
749        let stored = unit_law.law.to_stored(value.get()).map_err(|error| {
750            ParamError::new(
751                &self.key,
752                format!("{value} has no stored value: {error}"),
753                format!(
754                    "Use a value from {limits}; the nearest is {}",
755                    nearest(false)
756                ),
757            )
758            .with_source(error)
759        })?;
760        // A law round trip can land a few ulps past a bound; that is the bound.
761        let slack = 1e-9 * (self.stored_max - self.stored_min);
762        let stored = if (self.stored_max..=self.stored_max + slack).contains(&stored) {
763            self.stored_max
764        } else if (self.stored_min - slack..=self.stored_min).contains(&stored) {
765            self.stored_min
766        } else {
767            stored
768        };
769        if !(self.stored_min..=self.stored_max).contains(&stored) {
770            // The limits are in the unit of the value, so the message
771            // names one scale.
772            return Err(ParamError::new(
773                &self.key,
774                format!("{value} is outside {limits}"),
775                format!("Use the nearest, {}", nearest(stored > self.stored_max)),
776            ));
777        }
778        if self.kind != ParamKind::Float && stored.fract() != 0.0 {
779            return Err(ParamError::new(
780                &self.key,
781                format!("{value} is stored as {stored:?}, not a whole number"),
782                "Pick one whole step; the registry never rounds".to_owned(),
783            ));
784        }
785        Ok(stored)
786    }
787
788    /// The physical range as text, such as `12.978 Hz to 21096 Hz`, or the
789    /// stored range when a limit has no physical value.
790    fn physical_limits(&self) -> String {
791        match (
792            self.physical(self.stored_min),
793            self.physical(self.stored_max),
794        ) {
795            (Ok(low), Ok(high)) if low.get() <= high.get() => format!("{low} to {high}"),
796            (Ok(low), Ok(high)) => format!("{high} to {low}"),
797            _ => format!("stored {}", self.range_text()),
798        }
799    }
800}
801
802/// The parameter registry of one synth.
803pub trait ParamRegistry: fmt::Debug + Send + Sync {
804    /// The synth the registry describes.
805    fn synth(&self) -> Synth;
806
807    /// Every entry, sorted by key.
808    fn entries(&self) -> &[ParamSpec];
809
810    /// The entry of a key.
811    fn get(&self, key: &str) -> Option<&ParamSpec>;
812
813    /// The calibration tables loaded into the registry.
814    fn calibration(&self) -> &CalibrationSet;
815
816    /// Every entry, sorted by key.
817    fn iter(&self) -> std::slice::Iter<'_, ParamSpec> {
818        self.entries().iter()
819    }
820
821    /// Up to `limit` keys close to a wrong key, nearest first.
822    ///
823    /// A key qualifies when its edit distance is at most
824    /// `max(2, len / 3)` of the wrong key, or it contains the wrong text.
825    /// Ties sort by key. A wrong key longer than 64 characters has none:
826    /// no key is that long, and the distance would cost its length times
827    /// every key.
828    fn close_names(&self, wrong: &str, limit: usize) -> Vec<&str> {
829        if wrong.chars().nth(MAX_CLOSE_KEY).is_some() {
830            return Vec::new();
831        }
832        let threshold = (wrong.chars().count() / 3).max(2);
833        let mut ranked: Vec<(usize, &str)> = self
834            .iter()
835            .filter_map(|spec| {
836                let distance = edit_distance(wrong, &spec.key);
837                (distance <= threshold || (!wrong.is_empty() && spec.key.contains(wrong)))
838                    .then_some((distance, spec.key.as_str()))
839            })
840            .collect();
841        ranked.sort_unstable();
842        ranked.into_iter().take(limit).map(|(_, key)| key).collect()
843    }
844}
845
846/// The longest wrong key `close_names` compares.
847const MAX_CLOSE_KEY: usize = 64;
848
849/// The Levenshtein distance between two texts, by characters.
850fn edit_distance(a: &str, b: &str) -> usize {
851    let b: Vec<char> = b.chars().collect();
852    let mut row: Vec<usize> = (0..=b.len()).collect();
853    for (i, ca) in a.chars().enumerate() {
854        let mut diagonal = row[0];
855        row[0] = i + 1;
856        for (j, cb) in b.iter().enumerate() {
857            let next = (diagonal + usize::from(ca != *cb))
858                .min(row[j] + 1)
859                .min(row[j + 1] + 1);
860            diagonal = row[j + 1];
861            row[j + 1] = next;
862        }
863    }
864    row[b.len()]
865}
866
867/// A registry built from a list of entries.
868#[derive(Clone, Debug)]
869pub struct Registry {
870    synth: Synth,
871    entries: Vec<ParamSpec>,
872    index: HashMap<String, usize>,
873    calibration: CalibrationSet,
874}
875
876impl Registry {
877    /// Sorts the entries by key and indexes them.
878    ///
879    /// # Errors
880    ///
881    /// A message naming a key that appears twice.
882    pub fn new(synth: Synth, mut entries: Vec<ParamSpec>) -> Result<Self, String> {
883        entries.sort_by(|a, b| a.key.cmp(&b.key));
884        if let Some(pair) = entries.windows(2).find(|pair| pair[0].key == pair[1].key) {
885            return Err(format!("key {:?} appears twice", pair[0].key));
886        }
887        let index = entries
888            .iter()
889            .enumerate()
890            .map(|(i, spec)| (spec.key.clone(), i))
891            .collect();
892        Ok(Self {
893            synth,
894            entries,
895            index,
896            calibration: CalibrationSet {
897                synth,
898                tables: Vec::new(),
899            },
900        })
901    }
902
903    /// A copy of the registry with the laws of a calibration set. Each
904    /// table replaces the law of its key, sets the provenance to
905    /// [`Provenance::Measured`] and `verified_on` to the table's build.
906    ///
907    /// # Errors
908    ///
909    /// A [`CalibrationError`] when the set is for another synth, a key is
910    /// unknown or has no unit, or a table leaves the stored range.
911    pub fn with_calibration(&self, set: &CalibrationSet) -> Result<Self, CalibrationError> {
912        if set.synth != self.synth {
913            return Err(CalibrationError(format!(
914                "the calibration set is for {}, but the registry is for {}",
915                set.synth.name(),
916                self.synth.name()
917            )));
918        }
919        let mut calibrated = self.clone();
920        for table in &set.tables {
921            let Some(&i) = calibrated.index.get(&table.key) else {
922                return Err(CalibrationError(format!(
923                    "calibration table for unknown key {:?}",
924                    table.key
925                )));
926            };
927            let spec = &mut calibrated.entries[i];
928            let Some(unit_law) = spec.law.as_mut() else {
929                return Err(CalibrationError(format!(
930                    "{}: the control claims no physical unit, so a table cannot calibrate it",
931                    table.key
932                )));
933            };
934            let law = Table::new(table.points.clone(), table.interp)
935                .map_err(|cause| CalibrationError(format!("{}: {cause}", table.key)))?;
936            if let Some((stored, _)) = law
937                .points()
938                .iter()
939                .find(|(stored, _)| !(spec.stored_min..=spec.stored_max).contains(stored))
940            {
941                return Err(CalibrationError(format!(
942                    "{}: table point at stored {stored:?} is outside {}",
943                    table.key,
944                    spec.range_text()
945                )));
946            }
947            unit_law.law = Law::Table(law);
948            spec.provenance = Provenance::Measured;
949            spec.verified_on = Some(table.build.clone());
950            spec.evidence.clone_from(&table.evidence);
951            calibrated.calibration.tables.push(table.clone());
952        }
953        Ok(calibrated)
954    }
955}
956
957impl ParamRegistry for Registry {
958    fn synth(&self) -> Synth {
959        self.synth
960    }
961
962    fn entries(&self) -> &[ParamSpec] {
963        &self.entries
964    }
965
966    fn get(&self, key: &str) -> Option<&ParamSpec> {
967        self.index.get(key).map(|&i| &self.entries[i])
968    }
969
970    fn calibration(&self) -> &CalibrationSet {
971        &self.calibration
972    }
973}
974
975/// A measured law for one key, as a render or probe produced it.
976#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
977#[serde(deny_unknown_fields)]
978pub struct CalibrationTable {
979    /// The key it calibrates.
980    pub key: String,
981    /// The build it was measured on.
982    pub build: Build,
983    /// The render or probe behind it.
984    pub evidence: String,
985    /// The interpolation between points.
986    pub interp: Interp,
987    /// `(stored, physical)` points, stored values increasing.
988    pub points: Vec<(f64, f64)>,
989}
990
991/// The calibration tables of one synth.
992#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
993#[serde(deny_unknown_fields)]
994pub struct CalibrationSet {
995    /// The synth the tables belong to.
996    pub synth: Synth,
997    /// The tables.
998    pub tables: Vec<CalibrationTable>,
999}
1000
1001impl CalibrationSet {
1002    /// Reads a set from JSON: `{"synth": "vital", "tables": [...]}`.
1003    /// Unknown fields are an error.
1004    ///
1005    /// # Errors
1006    ///
1007    /// A [`CalibrationError`] with the reader's message and position.
1008    pub fn from_json(text: &str) -> Result<Self, CalibrationError> {
1009        serde_json::from_str(text)
1010            .map_err(|error| CalibrationError(format!("cannot read the calibration set: {error}")))
1011    }
1012}
1013
1014/// A calibration set that cannot load into a registry.
1015#[derive(Clone, Debug, PartialEq, Eq)]
1016pub struct CalibrationError(String);
1017
1018impl fmt::Display for CalibrationError {
1019    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1020        f.write_str(&self.0)
1021    }
1022}
1023
1024impl Error for CalibrationError {}
1025
1026/// The registry of a synth.
1027///
1028/// # Errors
1029///
1030/// [`SynthError::NotBuilt`] for a synth outside [`Synth::READY`], and
1031/// [`SynthError::BrokenData`] if the built-in data does not load.
1032pub fn registry_for(synth: Synth) -> Result<&'static dyn ParamRegistry, SynthError> {
1033    match synth.ready()? {
1034        Synth::Vital => {
1035            crate::preset::vital::registry().map(|registry| registry as &'static dyn ParamRegistry)
1036        }
1037        Synth::Serum2 => Err(SynthError::NotBuilt(synth)),
1038    }
1039}
1040
1041#[cfg(test)]
1042pub(crate) mod tests {
1043    use super::*;
1044    use proptest::prelude::*;
1045
1046    /// One entry per law form, for the round-trip and monotone tests.
1047    pub(crate) fn law_cases() -> Vec<(&'static str, Law, f64, f64, UnitKind)> {
1048        vec![
1049            (
1050                "affine",
1051                Law::Affine {
1052                    scale: 100.0,
1053                    offset: 0.0,
1054                },
1055                -1.0,
1056                1.0,
1057                UnitKind::PercentBipolar,
1058            ),
1059            (
1060                "power",
1061                Law::Power {
1062                    exponent: 4.0,
1063                    scale: 1.0,
1064                    offset: 0.0,
1065                },
1066                0.0,
1067                2.378_42,
1068                UnitKind::Seconds,
1069            ),
1070            (
1071                "exp2",
1072                Law::Exp2 {
1073                    rate: 1.0 / 12.0,
1074                    scale: 8.175_798_915_6,
1075                },
1076                8.0,
1077                136.0,
1078                UnitKind::Hz,
1079            ),
1080            (
1081                "db_of_power",
1082                Law::DbOfPower { exponent: 2.0 },
1083                0.001,
1084                1.0,
1085                UnitKind::Decibels,
1086            ),
1087            (
1088                "table",
1089                Law::Table(
1090                    Table::new(
1091                        vec![(0.0, 100.0), (0.5, 400.0), (1.0, 3200.0)],
1092                        Interp::LogPhysical,
1093                    )
1094                    .unwrap(),
1095                ),
1096                0.0,
1097                1.0,
1098                UnitKind::Hz,
1099            ),
1100            (
1101                "falling table",
1102                Law::Table(
1103                    Table::new(vec![(0.0, 6.0), (0.25, 0.0), (1.0, -30.0)], Interp::Linear)
1104                        .unwrap(),
1105                ),
1106                0.0,
1107                1.0,
1108                UnitKind::Decibels,
1109            ),
1110            (
1111                "custom",
1112                Law::Custom(CustomLaw(Arc::new(Cube))),
1113                0.0,
1114                1.0,
1115                UnitKind::Seconds,
1116            ),
1117        ]
1118    }
1119
1120    /// A law no closed form covers: `10 * s^3` seconds.
1121    #[derive(Debug)]
1122    struct Cube;
1123
1124    impl ParamLaw for Cube {
1125        fn to_physical(&self, stored: f64) -> Result<f64, RangeError> {
1126            Ok(10.0 * stored.powi(3))
1127        }
1128
1129        fn to_stored(&self, physical: f64) -> Result<f64, RangeError> {
1130            Ok((physical / 10.0).cbrt())
1131        }
1132    }
1133
1134    pub(crate) fn spec(key: &str, law: Law, min: f64, max: f64, unit: UnitKind) -> ParamSpec {
1135        ParamSpec {
1136            key: key.to_owned(),
1137            kind: ParamKind::Float,
1138            stored_min: min,
1139            stored_max: max,
1140            default: min,
1141            law: Some(UnitLaw { unit, law }),
1142            label: key.to_owned(),
1143            choices: Vec::new(),
1144            provenance: Provenance::FromFacts,
1145            evidence: "test".to_owned(),
1146            first_build: None,
1147            verified_on: None,
1148            tuning_note: None,
1149            legacy: false,
1150        }
1151    }
1152
1153    proptest! {
1154        #![proptest_config(ProptestConfig::with_cases(1000))]
1155        #[test]
1156        fn every_law_round_trips(t in 0.0..=1.0f64) {
1157            for (name, law, min, max, _) in law_cases() {
1158                let stored = min + t * (max - min);
1159                let back = law.to_stored(law.to_physical(stored).unwrap()).unwrap();
1160                prop_assert!(
1161                    (back - stored).abs() <= 1e-9 * stored.abs().max(1e-9),
1162                    "{name}: {stored} came back as {back}"
1163                );
1164            }
1165        }
1166
1167        #[test]
1168        fn every_law_is_monotone(a in 0.0..=1.0f64, step in 0.0..=1.0f64) {
1169            // b >= a by construction: rejecting half of the pairs exhausts
1170            // proptest's global reject limit.
1171            let b = a + (1.0 - a) * step;
1172            for (name, law, min, max, _) in law_cases() {
1173                let (sa, sb) = (min + a * (max - min), min + b * (max - min));
1174                let (pa, pb) = (law.to_physical(sa).unwrap(), law.to_physical(sb).unwrap());
1175                let rising = law.to_physical(max).unwrap() > law.to_physical(min).unwrap();
1176                prop_assert!(if rising { pa <= pb } else { pa >= pb }, "{name}: {sa} -> {pa}, {sb} -> {pb}");
1177                // Every law case is strictly monotone. Points a few ulps
1178                // apart may still round to one physical value.
1179                if sb - sa > 1e-9 * (max - min) {
1180                    prop_assert!(if rising { pa < pb } else { pa > pb }, "{name}: {sa} -> {pa}, {sb} -> {pb}");
1181                }
1182            }
1183        }
1184    }
1185
1186    #[test]
1187    fn values_outside_the_domain_name_the_key_and_range() {
1188        for (name, law, min, max, unit) in law_cases() {
1189            let entry = spec(name, law, min, max, unit);
1190            let message = entry.physical(max + 1.0).unwrap_err().to_string();
1191            assert!(message.starts_with(&format!("{name}: ")), "{message}");
1192            assert!(message.contains(&entry.range_text()), "{message}");
1193        }
1194        let power = spec(
1195            "env_1_attack",
1196            Law::Power {
1197                exponent: 4.0,
1198                scale: 1.0,
1199                offset: 0.0,
1200            },
1201            0.0,
1202            2.378_42,
1203            UnitKind::Seconds,
1204        );
1205        let message = power
1206            .stored(&Physical::new(UnitKind::Seconds, 40.0).unwrap())
1207            .unwrap_err()
1208            .to_string();
1209        assert!(
1210            message.starts_with("env_1_attack: 40 s is outside 0 s to 32"),
1211            "{message}"
1212        );
1213        assert!(message.contains("0 s to 32"), "{message}");
1214        let wrong_unit = power
1215            .stored(&Physical::new(UnitKind::Hz, 4.0).unwrap())
1216            .unwrap_err()
1217            .to_string();
1218        assert!(wrong_unit.contains("takes Seconds"), "{wrong_unit}");
1219    }
1220
1221    #[test]
1222    fn domain_edges_are_errors() {
1223        let power = Law::Power {
1224            exponent: 2.0,
1225            scale: 1.0,
1226            offset: 0.0,
1227        };
1228        assert!(power.to_physical(-0.1).is_err());
1229        assert!(power.to_stored(-1.0).is_err());
1230        let db = Law::DbOfPower { exponent: 2.0 };
1231        assert!(db.to_physical(0.0).is_err());
1232        let exp2 = Law::Exp2 {
1233            rate: 1.0,
1234            scale: 1.0,
1235        };
1236        assert!(exp2.to_stored(0.0).is_err());
1237        assert!(exp2.to_physical(f64::NAN).is_err());
1238        assert!(exp2.to_physical(5000.0).is_err());
1239    }
1240
1241    #[test]
1242    fn tables_never_extrapolate() {
1243        let table = Table::new(vec![(0.0, 1.0), (1.0, 2.0)], Interp::Linear).unwrap();
1244        assert_eq!(table.to_physical(0.5).unwrap(), 1.5);
1245        assert!(table.to_physical(1.01).is_err());
1246        assert!(table.to_stored(2.5).is_err());
1247    }
1248
1249    #[test]
1250    fn bad_tables_are_rejected() {
1251        assert!(Table::new(vec![(0.0, 1.0)], Interp::Linear).is_err());
1252        assert!(Table::new(vec![(0.0, 1.0), (0.0, 2.0)], Interp::Linear).is_err());
1253        assert!(Table::new(vec![(0.0, 1.0), (1.0, 2.0), (2.0, 1.5)], Interp::Linear).is_err());
1254        assert!(Table::new(vec![(0.0, 1.0), (1.0, 1.0)], Interp::Linear).is_err());
1255        assert!(Table::new(vec![(0.0, 0.0), (1.0, 1.0)], Interp::LogPhysical).is_err());
1256        assert!(Table::new(vec![(0.0, f64::NAN), (1.0, 1.0)], Interp::Linear).is_err());
1257    }
1258
1259    #[test]
1260    fn whole_kinds_never_round() {
1261        let mut entry = spec(
1262            "osc_1_transpose",
1263            Law::Affine {
1264                scale: 1.0,
1265                offset: 0.0,
1266            },
1267            -48.0,
1268            48.0,
1269            UnitKind::Semitones,
1270        );
1271        entry.kind = ParamKind::Count;
1272        assert_eq!(
1273            entry
1274                .stored(&Physical::new(UnitKind::Semitones, 12.0).unwrap())
1275                .unwrap(),
1276            12.0
1277        );
1278        let message = entry
1279            .stored(&Physical::new(UnitKind::Semitones, 12.5).unwrap())
1280            .unwrap_err()
1281            .to_string();
1282        assert!(message.contains("not a whole number"), "{message}");
1283        assert!(entry.physical(1.5).is_err());
1284    }
1285
1286    #[test]
1287    fn parse_uses_the_unit_string_parser() {
1288        assert_eq!(
1289            Physical::parse(UnitKind::Hz, "1.2 kHz").unwrap(),
1290            Physical::Hz(Hz::new(1200.0).unwrap())
1291        );
1292        let wrong = Physical::parse(UnitKind::Hz, "1.2 s").unwrap_err();
1293        assert_eq!(wrong, "1.2 s".parse::<Hz>().unwrap_err());
1294        assert!(Physical::parse(UnitKind::Normalized, "0.5").is_err());
1295        assert_eq!(
1296            Physical::parse(UnitKind::PercentBipolar, "-40%")
1297                .unwrap()
1298                .get(),
1299            -40.0
1300        );
1301    }
1302
1303    #[test]
1304    fn edit_distance_counts_single_changes() {
1305        assert_eq!(edit_distance("", "abc"), 3);
1306        assert_eq!(edit_distance("cutoff", "cutoff"), 0);
1307        assert_eq!(edit_distance("cuttoff", "cutoff"), 1);
1308        assert_eq!(edit_distance("kitten", "sitting"), 3);
1309    }
1310
1311    fn small_registry() -> Registry {
1312        let entries = law_cases()
1313            .into_iter()
1314            .map(|(name, law, min, max, unit)| spec(&name.replace(' ', "_"), law, min, max, unit))
1315            .chain([spec(
1316                "filter_1_cutoff",
1317                Law::Exp2 {
1318                    rate: 1.0 / 12.0,
1319                    scale: 8.175_798_915_6,
1320                },
1321                8.0,
1322                136.0,
1323                UnitKind::Hz,
1324            )])
1325            .collect();
1326        Registry::new(Synth::Vital, entries).unwrap()
1327    }
1328
1329    #[test]
1330    fn close_names_rank_by_distance() {
1331        let registry = small_registry();
1332        assert_eq!(
1333            registry.close_names("filter_1_cuttoff", 10).first(),
1334            Some(&"filter_1_cutoff")
1335        );
1336        assert_eq!(
1337            registry.close_names("table", 10),
1338            ["table", "falling_table"]
1339        );
1340        assert_eq!(registry.close_names("zzzzzzzzzzzz", 10), Vec::<&str>::new());
1341        assert_eq!(registry.close_names("filter", 1).len(), 1);
1342    }
1343
1344    #[test]
1345    fn duplicate_keys_are_rejected() {
1346        let one = small_registry().entries()[0].clone();
1347        assert!(Registry::new(Synth::Vital, vec![one.clone(), one]).is_err());
1348    }
1349
1350    #[test]
1351    fn calibration_turns_interim_into_measured() {
1352        let mut registry_entries = small_registry().entries().to_vec();
1353        for entry in &mut registry_entries {
1354            entry.provenance = Provenance::Interim;
1355            entry.tuning_note = Some("by ear".to_owned());
1356        }
1357        let registry = Registry::new(Synth::Vital, registry_entries).unwrap();
1358        let set = CalibrationSet::from_json(
1359            r#"{"synth":"vital","tables":[{"key":"filter_1_cutoff","build":"1.5.5","evidence":"render 1","interp":"log_physical","points":[[8.0,20.0],[136.0,20000.0]]}]}"#,
1360        )
1361        .unwrap();
1362        let calibrated = registry.with_calibration(&set).unwrap();
1363        let cutoff = calibrated.get("filter_1_cutoff").unwrap();
1364        assert_eq!(cutoff.provenance, Provenance::Measured);
1365        assert_eq!(cutoff.verified_on, Some(Build("1.5.5".to_owned())));
1366        assert_eq!(cutoff.evidence, "render 1");
1367        assert!(matches!(cutoff.law.as_ref().unwrap().law, Law::Table(_)));
1368        assert_eq!(calibrated.calibration().tables.len(), 1);
1369        assert_eq!(
1370            registry.get("filter_1_cutoff").unwrap().provenance,
1371            Provenance::Interim
1372        );
1373
1374        let unknown = CalibrationSet::from_json(
1375            r#"{"synth":"vital","tables":[{"key":"nope","build":"1","evidence":"e","interp":"linear","points":[[0,0],[1,1]]}]}"#,
1376        )
1377        .unwrap();
1378        assert!(
1379            registry
1380                .with_calibration(&unknown)
1381                .unwrap_err()
1382                .to_string()
1383                .contains("unknown key")
1384        );
1385        let outside = CalibrationSet::from_json(
1386            r#"{"synth":"vital","tables":[{"key":"filter_1_cutoff","build":"1","evidence":"e","interp":"linear","points":[[0,1],[200,2]]}]}"#,
1387        )
1388        .unwrap();
1389        assert!(registry.with_calibration(&outside).is_err());
1390        let serum = CalibrationSet {
1391            synth: Synth::Serum2,
1392            tables: Vec::new(),
1393        };
1394        assert!(registry.with_calibration(&serum).is_err());
1395    }
1396
1397    #[test]
1398    fn calibration_json_is_strict() {
1399        assert!(CalibrationSet::from_json(r#"{"synth":"vital","tables":[],"extra":1}"#).is_err());
1400        assert!(
1401            CalibrationSet::from_json(
1402                r#"{"synth":"vital","tables":[{"key":"k","build":"1","evidence":"e","interp":"linear","points":[[0,0],[1,1]],"note":""}]}"#
1403            )
1404            .is_err()
1405        );
1406    }
1407
1408    #[test]
1409    fn serum2_has_no_registry_yet() {
1410        assert_eq!(
1411            registry_for(Synth::Serum2).unwrap_err(),
1412            SynthError::NotBuilt(Synth::Serum2)
1413        );
1414    }
1415}