Skip to main content

nord_format/formats/nsmp/
kernel.rs

1//! The resampling kernel: source samples onto the field lattice.
2//!
3//! Field `f` samples the source at `t(f) = PITCH_NUM·f / PITCH_DEN`, and the value it
4//! stores is `Σ G[k][m]·x[⌊t⌋ − m]` over the [`TAPS`] taps of one phase of a stored
5//! table: `k` is the fractional part of `t` truncated to nine bits, so the bank has
6//! [`PHASES`] phases, and `G[k][m] = h(m + k/512)`, a Kaiser-windowed sinc on the
7//! half-open support `d ∈ [−15, 15)`:
8//!
9//! ```text
10//! h(d) = B·sinc(B·d) · I₀(β·√(1 − (d/15)²)) / I₀(β)
11//! B = 1.0429·17501/22050        β = 8
12//! ```
13//!
14//! The cutoff `B` sits 4.29 % above the field rate's Nyquist. The window is `1/I₀(β)`
15//! at its edge, not zero, so `d = −15` (phase 0, `m = −15`) carries a real tap and
16//! `d = +15` does not. Per-phase DC gain is the ideal kernel's, within `1e-4` of
17//! unity, with no per-phase normalisation.
18//!
19//! The arithmetic is single precision up to the sum. The table holds `h` rounded to
20//! `f32`; a 16-bit sample enters as the `f32` product of its value and `32767/32768`
21//! — the source's own unit as a fraction of full scale, a depth term rather than a
22//! kernel gain, since the instrument quantises in the source's units; each tap's
23//! product is an `f32`; the products are summed in `f64` and truncated toward zero
24//! once. A few taps are stored off the closed form and are listed by value in
25//! `MEASURED`. Where a tap is known only to within an `f32` rounding, a sum landing
26//! within that of a quantiser step can still store one count off the instrument's.
27//!
28//! The ratio is not baked into the bank: [`Kernel`] runs the same shape at another
29//! one, which is what a source at a rate other than
30//! [`SOURCE_RATE`](super::codec::SOURCE_RATE) needs. What moves with the ratio is the
31//! cutoff — it keeps the narrower of the two Nyquists, so a source faster than the
32//! target is band-limited to the target's and nothing folds back. At the measured
33//! ratio that is `B` to the bit, and the bank is the measured one.
34//!
35//! Inferred from specimens; not confirmed on hardware.
36
37use super::codec::{PITCH_DEN, PITCH_NUM};
38use std::sync::OnceLock;
39
40/// Phases in the bank: the fractional source position truncated to nine bits.
41pub const PHASES: usize = 512;
42
43/// Taps per phase: `m` from `−15` through `14`, the support `[−15, 15)` at every phase.
44pub const TAPS: usize = 30;
45
46/// The `m` of tap 0. Tap `j` weights the source sample at `⌊t⌋ − (j − FIRST)`.
47const FIRST: i128 = 15;
48
49/// Half-width of the support, in source samples.
50const HALF_WIDTH: f64 = 15.0;
51
52/// Cutoff of the sinc as a fraction of the source's Nyquist on the measured lattice:
53/// `1.0429·17501/22050`, the field rate's Nyquist and 4.29 % over.
54const B: f64 = 0.827_745_708_5;
55
56/// Kaiser window shape.
57const BETA: f64 = 8.0;
58
59/// A 16-bit sample's unit as a fraction of full scale; exact in `f32`.
60const DEPTH_16: f32 = 32767.0 / 32768.0;
61
62/// Taps whose stored value is not `h` rounded to `f32`, as `(phase, tap, value)`. The
63/// values are measured, not derived; each lies within `2e-7` of the closed form.
64const 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
155/// Modified Bessel function of the first kind, order zero, by its power series.
156fn 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
171/// `sin(πx)/(πx)`, on the magnitude so that mirrored taps are bit-identical.
172fn 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
181/// `h` of cutoff `b`: the kernel at `d` source samples from the sample a tap
182/// weights, zero off-support.
183fn 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
191/// The cutoff a lattice of `num` source samples per `den` fields is band-limited to,
192/// as a fraction of the source's Nyquist: the narrower of the two Nyquists, over by
193/// the same 4.29 % the measured bank carries. A faster source is cut at the field
194/// rate's Nyquist so that nothing folds back; a slower one keeps its own band.
195///
196/// Exactly [`B`] at the measured ratio, however the caller spells it.
197fn 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
203/// `h` of cutoff `b` rounded to `f32` at every lattice point of the `[phase][tap]`
204/// bank.
205fn 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
216/// [`closed_form_at`] at the measured cutoff.
217fn closed_form() -> Box<[[f32; TAPS]; PHASES]> {
218    closed_form_at(B)
219}
220
221/// Lazily build the `[phase][tap]` bank the instrument stores.
222pub 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
233/// Return field `f` as `(floor(source position), phase)` without index overflow, on a
234/// lattice of `num` source samples per `den` fields.
235pub 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
244/// [`lattice_at`] on the lattice the tap bank was measured for.
245pub fn lattice(field: usize) -> (i128, usize) {
246    lattice_at(field, PITCH_NUM, PITCH_DEN)
247}
248
249/// Sum field `f`'s tap products in `f64` over `bank`; samples outside `source` are
250/// zero.
251fn 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
271/// Sum field `f`'s tap products in `f64` on the lattice the tap bank was measured for;
272/// samples outside `source` are zero.
273pub fn accumulate(source: &[i16], field: usize) -> f64 {
274    accumulate_over(taps(), source, field, PITCH_NUM, PITCH_DEN)
275}
276
277/// Return a field in source units on the lattice the tap bank was measured for,
278/// truncating toward zero once after the full sum.
279pub fn field(source: &[i16], at: usize) -> i64 {
280    accumulate(source, at).trunc() as i64
281}
282
283/// The tap bank at one lattice ratio, for a source at a rate of its own.
284///
285/// The taps are the measured ones where the ratio is the measured ratio, whatever
286/// pair of rates spells it, and the same shape at that ratio's own [`cutoff`]
287/// otherwise. Build one per resampling run: a derived bank is computed on
288/// construction, not per field.
289pub struct Kernel {
290    num: u32,
291    den: u32,
292    bank: Option<Box<[[f32; TAPS]; PHASES]>>,
293}
294
295impl Kernel {
296    /// The kernel for a lattice of `num` source samples per `den` fields — a source
297    /// rate and a target rate, in that order, or any ratio equal to theirs.
298    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    /// Sum field `f`'s tap products in `f64`; samples outside `source` are zero.
312    pub fn accumulate(&self, source: &[i16], field: usize) -> f64 {
313        accumulate_over(self.taps(), source, field, self.num, self.den)
314    }
315
316    /// Return a field in source units, truncating toward zero once after the full sum.
317    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        // Two source samples per field lands on a stored sample every time.
410        for f in 0..8 {
411            assert_eq!(lattice_at(f, 2, 1), (2 * f as i128, 0));
412        }
413        // Three source samples per two fields alternates whole and half.
414        assert_eq!(lattice_at(1, 3, 2), (1, PHASES / 2));
415        assert_eq!(lattice_at(2, 3, 2), (3, 0));
416    }
417
418    /// The cutoff follows the ratio, and at the measured ratio it is the measured
419    /// value to the bit — however the caller spells that ratio — so the lattice the
420    /// bank was measured on runs the measured taps and nothing else does.
421    #[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        // A faster source keeps the same band, which is a smaller part of its own.
428        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        // A slower source keeps its whole band: the cutoff sits over its own Nyquist.
433        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    /// A field on the measured lattice is the same field whichever entry point asks
439    /// for it.
440    #[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        // t(1) = 22050/17501 = 1 + 4549/17501; 4549·512/17501 = 133.08.
453        assert_eq!(lattice(1), (1, 133));
454        // t(17501) = 22050 exactly.
455        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}