Skip to main content

secs_types/
target.rs

1//! Target curve: control points joined by a smoothstep in log frequency.
2//!
3//! Translated from SECS 2's `model.TargetCurve` (`src/secs/model.py`). The
4//! curve is a domain value shared by project files (`secs-project`, which
5//! reads and writes it) and the correction engine (`secs-engine`, which
6//! samples it), so its arithmetic lives in one place.
7
8use std::collections::BTreeMap;
9
10fn python_sort_run(points: &mut [(f64, f64)]) -> usize {
11    if points.len() < 2 {
12        return points.len();
13    }
14    let mut n = 1;
15    while n < points.len()
16        && points[n].0.partial_cmp(&points[n - 1].0) != Some(std::cmp::Ordering::Less)
17    {
18        n += 1;
19    }
20    if n == points.len() {
21        return n;
22    }
23    if n > 1 {
24        if points[0].0 < points[n - 1].0 {
25            return n;
26        }
27        points[..n].reverse();
28    }
29    n += 1;
30    let mut equal_tail = 0;
31    while n < points.len() {
32        if points[n].0 < points[n - 1].0 {
33            if equal_tail != 0 {
34                points[n - equal_tail - 1..n].reverse();
35                equal_tail = 0;
36            }
37        } else if points[n - 1].0 < points[n].0 {
38            break;
39        } else {
40            equal_tail += 1;
41        }
42        n += 1;
43    }
44    if equal_tail != 0 {
45        points[n - equal_tail - 1..n].reverse();
46    }
47    points[..n].reverse();
48    while n < points.len()
49        && points[n].0.partial_cmp(&points[n - 1].0) != Some(std::cmp::Ordering::Less)
50    {
51        n += 1;
52    }
53    n
54}
55
56/// A target response in dB over frequency in Hz, kept sorted by frequency.
57#[derive(Debug, Clone, PartialEq)]
58pub struct TargetCurve {
59    pub points: Vec<(f64, f64)>,
60}
61impl Default for TargetCurve {
62    fn default() -> Self {
63        Self::new(vec![
64            (25., 2.),
65            (80., 2.),
66            (200., 0.),
67            (1000., 0.),
68            (16000., -2.),
69        ])
70    }
71}
72impl TargetCurve {
73    pub fn new(points: Vec<(f64, f64)>) -> Self {
74        let mut s = Self { points };
75        s.sort();
76        s
77    }
78    pub fn sort(&mut self) {
79        // Target curves are shorter than CPython's TimSort minimum run. Match
80        // its natural-run detection before extending that run by binary sort;
81        // this comparison order matters for NaN, while +0/-0 remain stable.
82        let sorted = python_sort_run(&mut self.points);
83        for index in sorted..self.points.len() {
84            let point = self.points[index];
85            let mut lo = 0;
86            let mut hi = index;
87            while lo < hi {
88                let mid = (lo + hi) / 2;
89                if point.0 < self.points[mid].0 {
90                    hi = mid;
91                } else {
92                    lo = mid + 1;
93                }
94            }
95            self.points[lo..=index].rotate_right(1);
96        }
97    }
98    pub fn at(&self, f: f64) -> f64 {
99        if self.points.is_empty() {
100            return 0.;
101        }
102        if f <= self.points[0].0 {
103            return self.points[0].1;
104        }
105        if f >= self.points[self.points.len() - 1].0 {
106            return self.points[self.points.len() - 1].1;
107        }
108        for w in self.points.windows(2) {
109            let ((fa, da), (fb, db)) = (w[0], w[1]);
110            if fa <= f && f <= fb {
111                let t = (f.ln() - fa.ln()) / (fb.ln() - fa.ln());
112                let u = t * t * (3. - 2. * t);
113                return da + (db - da) * u;
114            }
115        }
116        0.
117    }
118    pub fn sample(&self, freqs: &[f64]) -> Vec<f64> {
119        freqs.iter().map(|&f| self.at(f)).collect()
120    }
121    pub fn presets() -> BTreeMap<&'static str, Self> {
122        [
123            (
124                "flat",
125                vec![(25., 0.), (200., 0.), (1000., 0.), (16000., 0.)],
126            ),
127            (
128                "harman",
129                vec![(25., 6.), (60., 5.), (120., 1.), (1000., 0.), (16000., -3.)],
130            ),
131            (
132                "cinema",
133                vec![
134                    (25., 4.),
135                    (80., 3.),
136                    (200., 0.),
137                    (1000., -1.),
138                    (16000., -5.),
139                ],
140            ),
141        ]
142        .into_iter()
143        .map(|(k, v)| (k, Self::new(v)))
144        .collect()
145    }
146}