1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ParamKind {
41 Float,
43 Count,
45 Flag,
47 Choice,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum UnitKind {
55 Hz,
57 Seconds,
59 Semitones,
61 Cents,
63 Octaves,
65 Decibels,
67 Normalized,
69 MidiPitch,
71 PercentUnipolar,
73 PercentBipolar,
75}
76
77impl UnitKind {
78 #[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#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum Physical {
99 Hz(Hz),
101 Seconds(Seconds),
103 Semitones(Semitones),
105 Cents(Cents),
107 Octaves(Octaves),
109 Decibels(Decibels),
111 Normalized(Normalized),
113 MidiPitch(MidiPitch),
115 PercentUnipolar(Percent<Unipolar>),
117 PercentBipolar(Percent<Bipolar>),
119}
120
121impl Physical {
122 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 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 #[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 #[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
220pub trait ParamLaw: fmt::Debug + Send + Sync {
226 fn to_physical(&self, stored: f64) -> Result<f64, RangeError>;
233
234 fn to_stored(&self, physical: f64) -> Result<f64, RangeError>;
241}
242
243#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum Law {
247 Affine {
249 scale: f64,
251 offset: f64,
253 },
254 Power {
256 exponent: f64,
258 scale: f64,
260 offset: f64,
262 },
263 Exp2 {
265 rate: f64,
267 scale: f64,
269 },
270 DbOfPower {
273 exponent: f64,
275 },
276 Table(Table),
278 #[serde(skip)]
280 Custom(CustomLaw),
281}
282
283#[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
293fn 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "snake_case")]
353pub enum Interp {
354 Linear,
356 LogPhysical,
359}
360
361#[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 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 #[must_use]
441 pub fn points(&self) -> &[(f64, f64)] {
442 &self.points
443 }
444
445 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
510#[serde(rename_all = "snake_case")]
511pub enum Provenance {
512 FromFacts,
514 Measured,
517 Interim,
520}
521
522#[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#[derive(Clone, Debug, PartialEq)]
536pub struct UnitLaw {
537 pub unit: UnitKind,
539 pub law: Law,
541}
542
543#[derive(Clone, Debug, PartialEq)]
545pub struct ParamSpec {
546 pub key: String,
548 pub kind: ParamKind,
550 pub stored_min: f64,
552 pub stored_max: f64,
554 pub default: f64,
556 pub law: Option<UnitLaw>,
558 pub label: String,
560 pub choices: Vec<String>,
563 pub provenance: Provenance,
565 pub evidence: String,
567 pub first_build: Option<Build>,
569 pub verified_on: Option<Build>,
572 pub tuning_note: Option<String>,
574 pub legacy: bool,
577}
578
579#[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 #[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 #[must_use]
629 pub fn unit(&self) -> Option<UnitKind> {
630 self.law.as_ref().map(|law| law.unit)
631 }
632
633 #[must_use]
635 pub fn range_text(&self) -> String {
636 format!("{:?} to {:?}", self.stored_min, self.stored_max)
637 }
638
639 #[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 #[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 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 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 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 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 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
802pub trait ParamRegistry: fmt::Debug + Send + Sync {
804 fn synth(&self) -> Synth;
806
807 fn entries(&self) -> &[ParamSpec];
809
810 fn get(&self, key: &str) -> Option<&ParamSpec>;
812
813 fn calibration(&self) -> &CalibrationSet;
815
816 fn iter(&self) -> std::slice::Iter<'_, ParamSpec> {
818 self.entries().iter()
819 }
820
821 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
846const MAX_CLOSE_KEY: usize = 64;
848
849fn 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#[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 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 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
977#[serde(deny_unknown_fields)]
978pub struct CalibrationTable {
979 pub key: String,
981 pub build: Build,
983 pub evidence: String,
985 pub interp: Interp,
987 pub points: Vec<(f64, f64)>,
989}
990
991#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
993#[serde(deny_unknown_fields)]
994pub struct CalibrationSet {
995 pub synth: Synth,
997 pub tables: Vec<CalibrationTable>,
999}
1000
1001impl CalibrationSet {
1002 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#[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
1026pub 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 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 #[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 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 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}