1use super::codec::{PITCH_DEN, PITCH_NUM};
38use std::sync::OnceLock;
39
40pub const PHASES: usize = 512;
42
43pub const TAPS: usize = 30;
45
46const FIRST: i128 = 15;
48
49const HALF_WIDTH: f64 = 15.0;
51
52const B: f64 = 0.827_745_708_5;
55
56const BETA: f64 = 8.0;
58
59const DEPTH_16: f32 = 32767.0 / 32768.0;
61
62const MEASURED: [(usize, usize, f32); 88] = [
65 (1, 16, 0.15957576),
66 (2, 19, -0.050585914),
67 (12, 21, 0.0012514186),
68 (15, 14, 0.18686648),
69 (19, 22, -0.009900425),
70 (20, 15, 0.82630205),
71 (20, 21, 0.00010499005),
72 (21, 16, 0.1264128),
73 (25, 13, -0.14319749),
74 (29, 8, -0.014026146),
75 (37, 21, -0.002284253),
76 (49, 13, -0.15284193),
77 (54, 9, 0.010980806),
78 (54, 16, 0.07440956),
79 (64, 23, 0.009098176),
80 (71, 8, -0.017184436),
81 (75, 21, -0.0073220395),
82 (77, 10, 0.001966087),
83 (84, 16, 0.030549219),
84 (89, 10, -0.00071834825),
85 (103, 13, -0.16755463),
86 (105, 10, -0.0043659243),
87 (128, 22, 0.000063097374),
88 (142, 20, 0.034269594),
89 (146, 12, 0.07262834),
90 (151, 17, -0.03939612),
91 (153, 23, 0.0044261394),
92 (174, 17, -0.024487488),
93 (184, 15, 0.7108851),
94 (184, 17, -0.018098239),
95 (185, 14, 0.49301848),
96 (187, 5, -0.00045713593),
97 (199, 5, -0.0008218217),
98 (203, 17, -0.0061836657),
99 (205, 17, -0.004949704),
100 (213, 23, 0.0011110727),
101 (215, 10, -0.029679712),
102 (228, 11, 0.013030337),
103 (231, 21, -0.020947903),
104 (234, 14, 0.5760287),
105 (238, 15, 0.63809144),
106 (242, 10, -0.03536856),
107 (245, 9, 0.03330808),
108 (246, 16, -0.13485077),
109 (253, 6, -0.0012877032),
110 (259, 23, -0.0012877032),
111 (266, 13, -0.13485077),
112 (267, 20, 0.03330808),
113 (270, 19, -0.03536856),
114 (274, 14, 0.63809144),
115 (278, 15, 0.5760287),
116 (281, 8, -0.020947903),
117 (284, 18, 0.013030337),
118 (297, 19, -0.029679712),
119 (299, 6, 0.0011110727),
120 (307, 12, -0.004949704),
121 (309, 12, -0.0061836657),
122 (325, 24, -0.00045713593),
123 (327, 15, 0.49301848),
124 (328, 12, -0.018098239),
125 (328, 14, 0.7108851),
126 (338, 12, -0.024487488),
127 (359, 6, 0.0044261394),
128 (361, 12, -0.03939612),
129 (366, 17, 0.07262834),
130 (370, 9, 0.034269594),
131 (405, 13, -0.00059061556),
132 (407, 19, -0.0043659243),
133 (409, 16, -0.16755463),
134 (423, 19, -0.00071834825),
135 (428, 13, 0.030549219),
136 (435, 19, 0.001966087),
137 (441, 21, -0.017184436),
138 (448, 6, 0.009098176),
139 (458, 13, 0.07440956),
140 (458, 20, 0.010980806),
141 (463, 16, -0.15284193),
142 (475, 8, -0.002284253),
143 (483, 21, -0.014026146),
144 (487, 16, -0.14319749),
145 (491, 13, 0.1264128),
146 (492, 14, 0.82630205),
147 (493, 7, -0.009900425),
148 (497, 15, 0.18686648),
149 (500, 8, 0.0012514186),
150 (508, 8, 0.0024095366),
151 (510, 10, -0.050585914),
152 (511, 13, 0.15957576),
153];
154
155fn bessel_i0(x: f64) -> f64 {
157 let quarter_square = (x / 2.0) * (x / 2.0);
158 let mut term = 1.0;
159 let mut sum = 1.0;
160 let mut k = 1.0;
161 loop {
162 term *= quarter_square / (k * k);
163 if term < sum * f64::EPSILON {
164 return sum;
165 }
166 sum += term;
167 k += 1.0;
168 }
169}
170
171fn sinc(x: f64) -> f64 {
173 let x = x.abs();
174 if x == 0.0 {
175 return 1.0;
176 }
177 let y = std::f64::consts::PI * x;
178 y.sin() / y
179}
180
181fn h_at(d: f64, b: f64) -> f64 {
184 if !(-HALF_WIDTH..HALF_WIDTH).contains(&d) {
185 return 0.0;
186 }
187 let u = d.abs() / HALF_WIDTH;
188 b * sinc(b * d) * bessel_i0(BETA * (1.0 - u * u).sqrt()) / bessel_i0(BETA)
189}
190
191fn cutoff(num: u32, den: u32) -> f64 {
198 let keep = f64::from(num.min(den)) / f64::from(num);
199 let measured = f64::from(PITCH_DEN) / f64::from(PITCH_NUM);
200 B * (keep / measured)
201}
202
203fn closed_form_at(b: f64) -> Box<[[f32; TAPS]; PHASES]> {
206 let mut bank = Box::new([[0.0; TAPS]; PHASES]);
207 for (phase, row) in bank.iter_mut().enumerate() {
208 let fraction = phase as f64 / PHASES as f64;
209 for (j, slot) in row.iter_mut().enumerate() {
210 *slot = h_at(j as f64 - FIRST as f64 + fraction, b) as f32;
211 }
212 }
213 bank
214}
215
216fn closed_form() -> Box<[[f32; TAPS]; PHASES]> {
218 closed_form_at(B)
219}
220
221pub fn taps() -> &'static [[f32; TAPS]; PHASES] {
223 static BANK: OnceLock<Box<[[f32; TAPS]; PHASES]>> = OnceLock::new();
224 BANK.get_or_init(|| {
225 let mut bank = closed_form();
226 for &(phase, tap, value) in &MEASURED {
227 bank[phase][tap] = value;
228 }
229 bank
230 })
231}
232
233pub fn lattice_at(field: usize, num: u32, den: u32) -> (i128, usize) {
236 let t = u128::from(num) * field as u128;
237 let remainder = t % u128::from(den);
238 (
239 (t / u128::from(den)) as i128,
240 (remainder * PHASES as u128 / u128::from(den)) as usize,
241 )
242}
243
244pub fn lattice(field: usize) -> (i128, usize) {
246 lattice_at(field, PITCH_NUM, PITCH_DEN)
247}
248
249fn accumulate_over(
252 bank: &[[f32; TAPS]; PHASES],
253 source: &[i16],
254 field: usize,
255 num: u32,
256 den: u32,
257) -> f64 {
258 let (base, phase) = lattice_at(field, num, den);
259 let row = &bank[phase];
260 let mut acc = 0.0f64;
261 for (j, &tap) in row.iter().enumerate() {
262 let at = base + FIRST - j as i128;
263 if at >= 0 && at < source.len() as i128 {
264 let sample = f32::from(source[at as usize]) * DEPTH_16;
265 acc += f64::from(sample * tap);
266 }
267 }
268 acc
269}
270
271pub fn accumulate(source: &[i16], field: usize) -> f64 {
274 accumulate_over(taps(), source, field, PITCH_NUM, PITCH_DEN)
275}
276
277pub fn field(source: &[i16], at: usize) -> i64 {
280 accumulate(source, at).trunc() as i64
281}
282
283pub struct Kernel {
290 num: u32,
291 den: u32,
292 bank: Option<Box<[[f32; TAPS]; PHASES]>>,
293}
294
295impl Kernel {
296 pub fn new(num: u32, den: u32) -> Kernel {
299 let cutoff = cutoff(num, den);
300 Kernel {
301 num,
302 den,
303 bank: (cutoff != B).then(|| closed_form_at(cutoff)),
304 }
305 }
306
307 fn taps(&self) -> &[[f32; TAPS]; PHASES] {
308 self.bank.as_deref().unwrap_or_else(|| taps())
309 }
310
311 pub fn accumulate(&self, source: &[i16], field: usize) -> f64 {
313 accumulate_over(self.taps(), source, field, self.num, self.den)
314 }
315
316 pub fn field(&self, source: &[i16], at: usize) -> i64 {
318 self.accumulate(source, at).trunc() as i64
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::super::codec::{FIELD_RATE, SOURCE_RATE};
325 use super::*;
326
327 #[test]
328 fn the_bessel_series_matches_tabulated_values() {
329 assert_eq!(bessel_i0(0.0), 1.0);
330 assert!((bessel_i0(1.0) - 1.266_065_877_752_008_4).abs() < 1e-15);
331 assert!((bessel_i0(8.0) - 427.564_115_721_804_74).abs() < 1e-12);
332 }
333
334 #[test]
335 fn the_support_is_half_open() {
336 let edge = (B * sinc(B * 15.0) / bessel_i0(BETA)) as f32;
337 assert_eq!(taps()[0][0], edge);
338 assert!((f64::from(edge) - 4.79e-5).abs() < 1e-7, "{edge}");
339 assert_eq!(h_at(15.0, B), 0.0);
340 assert_eq!(h_at(-15.0 - f64::EPSILON * 16.0, B), 0.0);
341 assert_ne!(h_at(-15.0, B), 0.0);
342 }
343
344 #[test]
345 fn the_closed_form_is_mirror_symmetric_to_the_bit() {
346 let bank = closed_form();
347 for phase in 1..PHASES {
348 for j in 0..TAPS {
349 assert_eq!(
350 bank[phase][j].to_bits(),
351 bank[PHASES - phase][TAPS - 1 - j].to_bits(),
352 "phase {phase} tap {j}"
353 );
354 }
355 }
356 }
357
358 #[test]
359 fn the_measured_taps_stay_within_a_rounding_of_the_closed_form() {
360 let mut points: Vec<(usize, usize)> = MEASURED.iter().map(|&(p, t, _)| (p, t)).collect();
361 points.sort_unstable();
362 points.dedup();
363 assert_eq!(points.len(), MEASURED.len());
364 for &(phase, tap, value) in &MEASURED {
365 assert!(phase < PHASES && tap < TAPS, "phase {phase} tap {tap}");
366 let ideal = h_at(tap as f64 - FIRST as f64 + phase as f64 / PHASES as f64, B);
367 let off = (f64::from(value) - ideal).abs();
368 assert!(off < 2e-7, "phase {phase} tap {tap}: {value} vs {ideal}");
369 assert_ne!(value, ideal as f32, "phase {phase} tap {tap}");
370 }
371 }
372
373 #[test]
374 fn every_phase_sums_near_unity() {
375 for (phase, row) in taps().iter().enumerate() {
376 let sum: f64 = row.iter().map(|&g| f64::from(g)).sum();
377 assert!((sum - 1.0).abs() < 1.5e-4, "phase {phase}: {sum}");
378 }
379 }
380
381 #[test]
382 fn a_constant_resamples_one_count_under_itself() {
383 let source = vec![1000i16; 4096];
384 for f in 20..3000 {
385 assert_eq!(field(&source, f), 999, "field {f}");
386 }
387 let source = vec![-1000i16; 4096];
388 for f in 20..3000 {
389 assert_eq!(field(&source, f), -999, "field {f}");
390 }
391 }
392
393 #[test]
394 fn products_are_single_precision() {
395 let mut source = vec![0i16; 64];
396 source[0] = 32767;
397 let sample = f32::from(32767i16) * DEPTH_16;
398 let expected = f64::from(sample * taps()[0][FIRST as usize]);
399 assert_eq!(accumulate(&source, 0), expected);
400 assert_ne!(
401 expected,
402 f64::from(sample) * f64::from(taps()[0][FIRST as usize])
403 );
404 }
405
406 #[test]
407 fn another_ratio_walks_the_same_bank() {
408 assert_eq!(lattice_at(7, PITCH_NUM, PITCH_DEN), lattice(7));
409 for f in 0..8 {
411 assert_eq!(lattice_at(f, 2, 1), (2 * f as i128, 0));
412 }
413 assert_eq!(lattice_at(1, 3, 2), (1, PHASES / 2));
415 assert_eq!(lattice_at(2, 3, 2), (3, 0));
416 }
417
418 #[test]
422 fn the_cutoff_keeps_the_narrower_nyquist() {
423 assert_eq!(cutoff(PITCH_NUM, PITCH_DEN), B);
424 assert_eq!(cutoff(SOURCE_RATE, FIELD_RATE), B);
425 assert!(Kernel::new(SOURCE_RATE, FIELD_RATE).bank.is_none());
426
427 let faster = cutoff(96_000, FIELD_RATE);
429 assert!(faster < B, "{faster}");
430 assert!((faster * 48_000.0 - B * f64::from(SOURCE_RATE) / 2.0).abs() < 1e-6);
431
432 let slower = cutoff(22_050, FIELD_RATE);
434 assert!(slower > 1.0, "{slower}");
435 assert!(Kernel::new(22_050, FIELD_RATE).bank.is_some());
436 }
437
438 #[test]
441 fn the_kernel_at_the_measured_ratio_is_the_free_function() {
442 let source: Vec<i16> = (0..512).map(|n| ((n * 37) % 9001 - 4500) as i16).collect();
443 let kernel = Kernel::new(PITCH_NUM, PITCH_DEN);
444 for f in 0..300 {
445 assert_eq!(kernel.field(&source, f), field(&source, f), "field {f}");
446 }
447 }
448
449 #[test]
450 fn the_phase_truncates_the_fraction() {
451 assert_eq!(lattice(0), (0, 0));
452 assert_eq!(lattice(1), (1, 133));
454 assert_eq!(lattice(17501), (22050, 0));
456 }
457
458 #[test]
459 fn one_impulse_lights_the_kernels_support() {
460 let mut source = vec![0i16; 4096];
461 source[2048] = 30_000;
462 let lit: Vec<usize> = (0..3000).filter(|&f| field(&source, f) != 0).collect();
463 let (near, _) = lattice(lit[0]);
464 let (far, _) = lattice(lit[lit.len() - 1]);
465 assert!(2048 - near <= 15 && far - 2048 <= 14, "{near}..{far}");
466 assert!(2048 - near >= 13 && far - 2048 >= 13, "{near}..{far}");
467 }
468}