Skip to main content

ph_curves/
math.rs

1//! Fixed-point math helpers and the [`UnitValue`] abstraction.
2//!
3//! All arithmetic uses the [`fixed`] crate or plain integers so that the
4//! library remains `no_std` and avoids floating-point operations at runtime.
5
6use fixed::types::{I16F16, I32F32, U16F16};
7
8/// Rounding policy used by [`quantize`] and the tickless scheduler.
9#[derive(Copy, Clone, Debug, Eq, PartialEq)]
10pub enum Rounding {
11    /// Round down to the nearest multiple of `step`.
12    Floor,
13    /// Round up to the nearest multiple of `step`.
14    Ceil,
15    /// Round to the nearest multiple of `step` (half-up).
16    Nearest,
17}
18
19// ---------------------------------------------------------------------------
20// UnitValue trait
21// ---------------------------------------------------------------------------
22
23/// A normalized value type usable as a curve domain / range.
24///
25/// Implementors map a unit interval onto a discrete integer type so that the
26/// tickless scheduler can convert between wall-clock time fractions and curve
27/// positions.
28pub trait UnitValue: Copy + 'static {
29    /// The value representing 0 (start of the interval).
30    fn zero() -> Self;
31    /// The value representing 1 (end of the interval).
32    fn one() -> Self;
33
34    /// Convert this value to a LUT index (0-based).
35    fn to_index(self) -> usize;
36
37    /// Create a value from a time fraction `elapsed / duration`.
38    ///
39    /// Returns [`Self::zero()`] when `elapsed == 0` and [`Self::one()`] when
40    /// `elapsed >= duration`.
41    fn from_time_frac(elapsed_ms: u32, duration_ms: u32) -> Self;
42
43    /// Convert this value back to a wall-clock offset in milliseconds within
44    /// `duration_ms`, rounding up.
45    fn to_time_offset(self, duration_ms: u32) -> u32;
46
47    /// Linearly interpolate between two `u16` endpoints using `self` as the
48    /// blend weight.
49    fn lerp_u16(self, a: u16, b: u16) -> u16;
50
51    /// Inverse linear interpolation: find the blend weight `T` such that
52    /// `T::lerp_u16(a, b) ≈ target`, rounding toward `b`.
53    fn inv_lerp_u16(a: u16, b: u16, target: u16) -> Self;
54}
55
56// ---------------------------------------------------------------------------
57// UnitValue implementation for u8
58// ---------------------------------------------------------------------------
59
60impl UnitValue for u8 {
61    fn zero() -> Self {
62        0
63    }
64
65    fn one() -> Self {
66        255
67    }
68
69    fn to_index(self) -> usize {
70        self as usize
71    }
72
73    fn from_time_frac(elapsed_ms: u32, duration_ms: u32) -> Self {
74        if duration_ms == 0 || elapsed_ms >= duration_ms {
75            return 255;
76        }
77        if elapsed_ms == 0 {
78            return 0;
79        }
80        (u64::from(elapsed_ms) * 255 / u64::from(duration_ms)) as u8
81    }
82
83    fn to_time_offset(self, duration_ms: u32) -> u32 {
84        if duration_ms == 0 {
85            return 0;
86        }
87        // Splitting `duration_ms` as `255 * k + r` keeps this in 32-bit
88        // arithmetic: `ceil(v * d / 255) == v * k + ceil(v * r / 255)`.
89        // `v * k` peaks at exactly `u32::MAX` and `v * r` at 255 * 254, so
90        // neither term overflows, and the sum is still bounded by
91        // `duration_ms`.  A 64-bit divide costs ~2.3x as much on Cortex-M0.
92        let v = u32::from(self);
93        let k = duration_ms / 255;
94        let r = duration_ms % 255;
95        v * k + (v * r).div_ceil(255)
96    }
97
98    fn lerp_u16(self, a: u16, b: u16) -> u16 {
99        let a_fix = I32F32::from_num(a);
100        let b_fix = I32F32::from_num(b);
101        let t = I32F32::from_num(self) / I32F32::from_num(255);
102        let result = a_fix + t * (b_fix - a_fix);
103        result.to_num::<i64>().clamp(0, u16::MAX as i64) as u16
104    }
105
106    fn inv_lerp_u16(a: u16, b: u16, target: u16) -> Self {
107        if a == b {
108            return 255;
109        }
110        let delta = I32F32::from_num(b) - I32F32::from_num(a);
111        let numer = I32F32::from_num(target) - I32F32::from_num(a);
112        let frac = numer / delta;
113        let w = (frac * I32F32::from_num(255)).ceil();
114        w.to_num::<i32>().clamp(0, 255) as u8
115    }
116}
117
118// ---------------------------------------------------------------------------
119// UnitValue implementation for u16
120// ---------------------------------------------------------------------------
121
122impl UnitValue for u16 {
123    fn zero() -> Self {
124        0
125    }
126
127    fn one() -> Self {
128        65535
129    }
130
131    fn to_index(self) -> usize {
132        self as usize
133    }
134
135    fn from_time_frac(elapsed_ms: u32, duration_ms: u32) -> Self {
136        if duration_ms == 0 || elapsed_ms >= duration_ms {
137            return 65535;
138        }
139        if elapsed_ms == 0 {
140            return 0;
141        }
142        // `u64` rather than `I32F32`: the fixed-point type holds only 32
143        // integer bits, so durations above `i32::MAX` overflowed on
144        // conversion.  The guard above bounds the quotient below 65535.
145        (u64::from(elapsed_ms) * 65535 / u64::from(duration_ms)) as u16
146    }
147
148    fn to_time_offset(self, duration_ms: u32) -> u32 {
149        if duration_ms == 0 {
150            return 0;
151        }
152        // As for `u8`: `ceil(v * d / 65535) == v * k + ceil(v * r / 65535)`
153        // where `d == 65535 * k + r`.  `v * k` peaks at exactly `u32::MAX`
154        // and `v * r` at `65535 * 65534`, so both stay in 32 bits.
155        let v = u32::from(self);
156        let k = duration_ms / 65535;
157        let r = duration_ms % 65535;
158        v * k + (v * r).div_ceil(65535)
159    }
160
161    fn lerp_u16(self, a: u16, b: u16) -> u16 {
162        let a_fix = I32F32::from_num(a);
163        let b_fix = I32F32::from_num(b);
164        let t = I32F32::from_num(self) / I32F32::from_num(65535);
165        let result = a_fix + t * (b_fix - a_fix);
166        result.to_num::<i64>().clamp(0, u16::MAX as i64) as u16
167    }
168
169    fn inv_lerp_u16(a: u16, b: u16, target: u16) -> Self {
170        if a == b {
171            return 65535;
172        }
173        let delta = I32F32::from_num(b) - I32F32::from_num(a);
174        let numer = I32F32::from_num(target) - I32F32::from_num(a);
175        let frac = numer / delta;
176        let w = (frac * I32F32::from_num(65535)).ceil();
177        w.to_num::<i64>().clamp(0, 65535) as u16
178    }
179}
180
181// ---------------------------------------------------------------------------
182// Interpolation helpers
183// ---------------------------------------------------------------------------
184
185/// Convert a `u8` weight (`0..=255`) to a fixed-point fraction in `[0, 1]`.
186fn weight_frac(w: u8) -> I16F16 {
187    I16F16::from_num(w) / I16F16::from_num(255)
188}
189
190/// Linearly interpolate between `a` and `b` with a `u8` blend weight.
191///
192/// `w = 0` returns `a`, `w = 255` returns `b`.  Intermediate values are
193/// computed with fixed-point arithmetic and clamped to `0..=255`.
194pub fn lerp_u8(a: u8, b: u8, w: u8) -> u8 {
195    let a_fix = I16F16::from_num(a);
196    let b_fix = I16F16::from_num(b);
197    let t = weight_frac(w);
198    let result = a_fix + t * (b_fix - a_fix);
199    result.to_num::<i32>().clamp(0, 255) as u8
200}
201
202/// Linearly interpolate between two `u16` values with a `u8` blend weight.
203///
204/// `w = 0` returns `a`, `w = 255` returns `b`.  The result is clamped to
205/// `0..=u16::MAX`.
206pub fn lerp_u16(a: u16, b: u16, w: u8) -> u16 {
207    let a_fix = I32F32::from_num(a);
208    let b_fix = I32F32::from_num(b);
209    let t = I32F32::from_num(w) / I32F32::from_num(255);
210    let result = a_fix + t * (b_fix - a_fix);
211    result.to_num::<i64>().clamp(0, u16::MAX as i64) as u16
212}
213
214/// Scale a normalized `u8` value (`0..=255`) into a `u16` range `0..=max`.
215///
216/// `w = 0` returns `0`, `w = 255` returns `max`.
217pub fn map_u8_to_u16(w: u8, max: u16) -> u16 {
218    let t = U16F16::from_num(w) / U16F16::from_num(255);
219    let result = t * U16F16::from_num(max);
220    result.to_num::<u32>().min(u16::MAX as u32) as u16
221}
222
223// ---------------------------------------------------------------------------
224// Quantization helpers
225// ---------------------------------------------------------------------------
226
227/// Snap `value` to the nearest multiple of `step` according to `rounding`.
228///
229/// # Panics
230///
231/// Panics if `step` is `0` (division by zero).
232pub fn quantize(value: u16, step: u16, rounding: Rounding) -> u16 {
233    let v = u32::from(value);
234    let s = u32::from(step);
235    let result = match rounding {
236        Rounding::Floor => (v / s) * s,
237        Rounding::Ceil => v.div_ceil(s) * s,
238        Rounding::Nearest => ((v + s / 2) / s) * s,
239    };
240    result.min(u32::from(u16::MAX)) as u16
241}
242
243/// Return the next quantized target value one `step` closer to `end`.
244///
245/// If `increasing` is `true` the value advances upward; otherwise downward.
246/// The result is clamped so it never overshoots `end`.
247pub fn next_target_value(current: u16, end: u16, step: u16, increasing: bool) -> u16 {
248    if increasing {
249        let next = current.saturating_add(step);
250        next.min(end)
251    } else {
252        let next = current.saturating_sub(step);
253        next.max(end)
254    }
255}