1#![allow(dead_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum InterpMethod {
11 Parabolic,
13 Gaussian,
15 Sinc,
17 Linear,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct InterpConfig {
24 pub method: InterpMethod,
26 pub radius: usize,
28 pub frame_rate: f64,
30 pub min_peak_height: f64,
32}
33
34impl Default for InterpConfig {
35 fn default() -> Self {
36 Self {
37 method: InterpMethod::Parabolic,
38 radius: 1,
39 frame_rate: 30.0,
40 min_peak_height: 0.1,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct SubFrameOffset {
48 pub frame_offset: i64,
50 pub fractional: f64,
52 pub peak_value: f64,
54 pub confidence: f64,
56}
57
58impl SubFrameOffset {
59 #[must_use]
61 pub fn new(frame_offset: i64, fractional: f64, peak_value: f64, confidence: f64) -> Self {
62 Self {
63 frame_offset,
64 fractional,
65 peak_value,
66 confidence: confidence.clamp(0.0, 1.0),
67 }
68 }
69
70 #[must_use]
72 pub fn total_frames(&self) -> f64 {
73 self.frame_offset as f64 + self.fractional
74 }
75
76 #[must_use]
78 pub fn to_seconds(&self, frame_rate: f64) -> f64 {
79 if frame_rate > 0.0 {
80 self.total_frames() / frame_rate
81 } else {
82 0.0
83 }
84 }
85
86 #[must_use]
88 pub fn to_millis(&self, frame_rate: f64) -> f64 {
89 self.to_seconds(frame_rate) * 1000.0
90 }
91}
92
93#[must_use]
98pub fn parabolic_interpolation(y_minus: f64, y_center: f64, y_plus: f64) -> (f64, f64) {
99 let denom = y_minus - 2.0 * y_center + y_plus;
100 if denom.abs() < 1e-15 {
101 return (0.0, y_center);
102 }
103 let delta = 0.5 * (y_minus - y_plus) / denom;
104 let peak = y_center - 0.25 * (y_minus - y_plus) * delta;
105 (delta.clamp(-0.5, 0.5), peak)
106}
107
108#[must_use]
112pub fn gaussian_interpolation(y_minus: f64, y_center: f64, y_plus: f64) -> (f64, f64) {
113 if y_minus <= 0.0 || y_center <= 0.0 || y_plus <= 0.0 {
115 return parabolic_interpolation(y_minus, y_center, y_plus);
116 }
117 let ln_minus = y_minus.ln();
118 let ln_center = y_center.ln();
119 let ln_plus = y_plus.ln();
120
121 let denom = 2.0 * (ln_minus - 2.0 * ln_center + ln_plus);
122 if denom.abs() < 1e-15 {
123 return (0.0, y_center);
124 }
125 let delta = (ln_minus - ln_plus) / denom;
126 let clamped = delta.clamp(-0.5, 0.5);
127 let peak = (ln_center
128 - 0.125 * (ln_minus - ln_plus).powi(2) / (ln_minus - 2.0 * ln_center + ln_plus))
129 .exp();
130 (clamped, peak)
131}
132
133#[allow(clippy::cast_precision_loss)]
135#[must_use]
136pub fn sinc_interpolation(
137 samples: &[f64],
138 center_idx: usize,
139 fractional: f64,
140 radius: usize,
141) -> f64 {
142 if samples.is_empty() {
143 return 0.0;
144 }
145 let mut sum = 0.0;
146 let mut weight_sum = 0.0;
147
148 let start = center_idx.saturating_sub(radius);
149 let end = (center_idx + radius + 1).min(samples.len());
150
151 for i in start..end {
152 let x = i as f64 - center_idx as f64 - fractional;
153 let w = sinc(x) * hann_window(x, radius as f64);
154 sum += samples[i] * w;
155 weight_sum += w;
156 }
157
158 if weight_sum.abs() > 1e-15 {
159 sum / weight_sum
160 } else {
161 samples.get(center_idx).copied().unwrap_or(0.0)
162 }
163}
164
165#[must_use]
167fn sinc(x: f64) -> f64 {
168 if x.abs() < 1e-15 {
169 1.0
170 } else {
171 let px = std::f64::consts::PI * x;
172 px.sin() / px
173 }
174}
175
176#[must_use]
178fn hann_window(x: f64, radius: f64) -> f64 {
179 if x.abs() > radius {
180 0.0
181 } else {
182 0.5 * (1.0 + (std::f64::consts::PI * x / radius).cos())
183 }
184}
185
186#[allow(clippy::cast_precision_loss)]
188#[must_use]
189pub fn find_subframe_peak(correlation: &[f64], config: &InterpConfig) -> Option<SubFrameOffset> {
190 if correlation.len() < 3 {
191 return None;
192 }
193
194 let (peak_idx, &peak_val) = correlation
196 .iter()
197 .enumerate()
198 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))?;
199
200 if peak_val < config.min_peak_height {
201 return None;
202 }
203
204 if peak_idx == 0 || peak_idx == correlation.len() - 1 {
206 let conf = compute_peak_confidence(correlation, peak_idx);
207 return Some(SubFrameOffset::new(peak_idx as i64, 0.0, peak_val, conf));
208 }
209
210 let y_minus = correlation[peak_idx - 1];
211 let y_center = correlation[peak_idx];
212 let y_plus = correlation[peak_idx + 1];
213
214 let (delta, interp_peak) = match config.method {
215 InterpMethod::Parabolic => parabolic_interpolation(y_minus, y_center, y_plus),
216 InterpMethod::Gaussian => gaussian_interpolation(y_minus, y_center, y_plus),
217 InterpMethod::Sinc => {
218 let (para_delta, _) = parabolic_interpolation(y_minus, y_center, y_plus);
219 let val = sinc_interpolation(correlation, peak_idx, para_delta, config.radius);
220 (para_delta, val)
221 }
222 InterpMethod::Linear => {
223 if y_minus > y_plus {
225 let d = -0.5 * (y_center - y_minus) / (y_center - y_minus + f64::EPSILON);
226 (d.clamp(-0.5, 0.0), y_center)
227 } else {
228 let d = 0.5 * (y_center - y_plus) / (y_center - y_plus + f64::EPSILON);
229 (d.clamp(0.0, 0.5), y_center)
230 }
231 }
232 };
233
234 let confidence = compute_peak_confidence(correlation, peak_idx);
235 Some(SubFrameOffset::new(
236 peak_idx as i64,
237 delta,
238 interp_peak,
239 confidence,
240 ))
241}
242
243fn compute_peak_confidence(correlation: &[f64], peak_idx: usize) -> f64 {
245 let peak_val = correlation[peak_idx];
246 if peak_val.abs() < 1e-15 {
247 return 0.0;
248 }
249
250 let mut second_max = 0.0_f64;
252 for (i, &val) in correlation.iter().enumerate() {
253 if i != peak_idx && val > second_max {
254 let is_local = i == 0
256 || i == correlation.len() - 1
257 || (val >= correlation[i.saturating_sub(1)]
258 && val >= correlation[(i + 1).min(correlation.len() - 1)]);
259 if is_local {
260 second_max = val;
261 }
262 }
263 }
264
265 if second_max.abs() < 1e-15 {
266 return 1.0;
267 }
268 let ratio = 1.0 - (second_max / peak_val);
269 ratio.clamp(0.0, 1.0)
270}
271
272#[must_use]
274pub fn lerp(a: f64, b: f64, t: f64) -> f64 {
275 a + (b - a) * t
276}
277
278#[must_use]
280pub fn cubic_hermite(y0: f64, y1: f64, y2: f64, y3: f64, t: f64) -> f64 {
281 let a = -0.5 * y0 + 1.5 * y1 - 1.5 * y2 + 0.5 * y3;
282 let b = y0 - 2.5 * y1 + 2.0 * y2 - 0.5 * y3;
283 let c = -0.5 * y0 + 0.5 * y2;
284 let d = y1;
285 ((a * t + b) * t + c) * t + d
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_parabolic_symmetric_peak() {
294 let (delta, peak) = parabolic_interpolation(0.5, 1.0, 0.5);
296 assert!(delta.abs() < 1e-10);
297 assert!((peak - 1.0).abs() < 1e-10);
298 }
299
300 #[test]
301 fn test_parabolic_asymmetric_peak() {
302 let (delta, _) = parabolic_interpolation(0.8, 1.0, 0.4);
304 assert!(delta < 0.0); }
306
307 #[test]
308 fn test_parabolic_flat() {
309 let (delta, peak) = parabolic_interpolation(1.0, 1.0, 1.0);
310 assert!((delta).abs() < 1e-10);
311 assert!((peak - 1.0).abs() < 1e-10);
312 }
313
314 #[test]
315 fn test_gaussian_symmetric() {
316 let (delta, _) = gaussian_interpolation(0.5, 1.0, 0.5);
317 assert!(delta.abs() < 1e-10);
318 }
319
320 #[test]
321 fn test_gaussian_fallback_on_negative() {
322 let (delta, _) = gaussian_interpolation(-0.5, 1.0, 0.5);
323 assert!(delta.abs() < 1.0);
325 }
326
327 #[test]
328 fn test_sinc_at_integer() {
329 let samples = vec![0.0, 0.0, 1.0, 0.0, 0.0];
330 let val = sinc_interpolation(&samples, 2, 0.0, 2);
331 assert!((val - 1.0).abs() < 0.01);
332 }
333
334 #[test]
335 fn test_sinc_empty() {
336 let val = sinc_interpolation(&[], 0, 0.0, 2);
337 assert!((val).abs() < f64::EPSILON);
338 }
339
340 #[test]
341 fn test_subframe_offset_total() {
342 let offset = SubFrameOffset::new(10, 0.25, 0.9, 0.95);
343 assert!((offset.total_frames() - 10.25).abs() < 1e-10);
344 }
345
346 #[test]
347 fn test_subframe_offset_to_seconds() {
348 let offset = SubFrameOffset::new(30, 0.0, 1.0, 0.9);
349 assert!((offset.to_seconds(30.0) - 1.0).abs() < 1e-10);
350 }
351
352 #[test]
353 fn test_subframe_offset_to_millis() {
354 let offset = SubFrameOffset::new(30, 0.0, 1.0, 0.9);
355 assert!((offset.to_millis(30.0) - 1000.0).abs() < 1e-10);
356 }
357
358 #[test]
359 fn test_subframe_offset_zero_fps() {
360 let offset = SubFrameOffset::new(10, 0.5, 1.0, 0.9);
361 assert!((offset.to_seconds(0.0)).abs() < f64::EPSILON);
362 }
363
364 #[test]
365 fn test_find_subframe_peak_parabolic() {
366 let correlation = vec![0.1, 0.3, 0.5, 0.9, 0.5, 0.3, 0.1];
367 let config = InterpConfig {
368 method: InterpMethod::Parabolic,
369 ..InterpConfig::default()
370 };
371 let result = find_subframe_peak(&correlation, &config);
372 assert!(result.is_some());
373 let r = result.expect("r should be valid");
374 assert_eq!(r.frame_offset, 3);
375 assert!(r.fractional.abs() < 0.5);
376 }
377
378 #[test]
379 fn test_find_subframe_peak_gaussian() {
380 let correlation = vec![0.2, 0.5, 1.0, 0.5, 0.2];
381 let config = InterpConfig {
382 method: InterpMethod::Gaussian,
383 ..InterpConfig::default()
384 };
385 let result = find_subframe_peak(&correlation, &config);
386 assert!(result.is_some());
387 let r = result.expect("r should be valid");
388 assert_eq!(r.frame_offset, 2);
389 }
390
391 #[test]
392 fn test_find_subframe_peak_too_short() {
393 let correlation = vec![0.5, 0.9];
394 let config = InterpConfig::default();
395 assert!(find_subframe_peak(&correlation, &config).is_none());
396 }
397
398 #[test]
399 fn test_find_subframe_peak_below_threshold() {
400 let correlation = vec![0.01, 0.02, 0.01];
401 let config = InterpConfig {
402 min_peak_height: 0.5,
403 ..InterpConfig::default()
404 };
405 assert!(find_subframe_peak(&correlation, &config).is_none());
406 }
407
408 #[test]
409 fn test_lerp() {
410 assert!((lerp(0.0, 1.0, 0.5) - 0.5).abs() < 1e-10);
411 assert!((lerp(2.0, 4.0, 0.25) - 2.5).abs() < 1e-10);
412 }
413
414 #[test]
415 fn test_cubic_hermite_at_endpoints() {
416 let v = cubic_hermite(0.0, 1.0, 2.0, 3.0, 0.0);
417 assert!((v - 1.0).abs() < 1e-10);
418 }
419
420 #[test]
421 fn test_subframe_confidence_clamped() {
422 let offset = SubFrameOffset::new(0, 0.0, 1.0, 1.5);
423 assert!((offset.confidence - 1.0).abs() < f64::EPSILON);
424
425 let offset2 = SubFrameOffset::new(0, 0.0, 1.0, -0.5);
426 assert!((offset2.confidence).abs() < f64::EPSILON);
427 }
428}