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}