mfsk_core/ft8/
downsample.rs1use crate::core::dsp::downsample::{self as g, DownsampleCfg};
10use num_complex::Complex;
11
12pub const FT8_CFG: DownsampleCfg = DownsampleCfg {
14 input_rate: 12_000,
15 fft1_size: 192_000,
16 fft2_size: 3_200,
17 tone_spacing_hz: 6.25,
18 leading_pad_tones: 1.5,
19 trailing_pad_tones: 1.5,
20 ntones: 8,
21 edge_taper_bins: 101,
22};
23
24#[inline]
28pub fn downsample(
29 audio: &[i16],
30 f0: f32,
31 fft_cache: Option<&[Complex<f32>]>,
32) -> (Vec<Complex<f32>>, Vec<Complex<f32>>) {
33 match fft_cache {
34 Some(cache) => (g::downsample_cached(cache, f0, &FT8_CFG), cache.to_vec()),
35 None => g::downsample(audio, f0, &FT8_CFG),
36 }
37}
38
39#[inline]
42pub fn build_fft_cache(audio: &[i16]) -> Vec<Complex<f32>> {
43 g::build_fft_cache(audio, &FT8_CFG)
44}
45
46#[inline]
48pub fn downsample_simple(audio: &[i16], f0: f32) -> Vec<Complex<f32>> {
49 downsample(audio, f0, None).0
50}
51
52#[cfg(test)]
53mod tests {
54 use super::super::params::NMAX;
55 use super::*;
56
57 const NFFT2: usize = 3_200;
58
59 #[test]
60 fn sine_at_f0_energy_at_dc() {
61 let f0 = 1000.0f32;
62 let audio: Vec<i16> = (0..NMAX)
63 .map(|n| {
64 let t = n as f32 / 12_000.0;
65 (10_000.0 * (2.0 * std::f32::consts::PI * f0 * t).sin()) as i16
66 })
67 .collect();
68
69 let out = downsample_simple(&audio, f0);
70
71 let mut spectrum = out.clone();
72 let mut planner = rustfft::FftPlanner::<f32>::new();
73 planner.plan_fft_forward(NFFT2).process(&mut spectrum);
74
75 let energy_near_dc: f32 = spectrum[..=10]
76 .iter()
77 .chain(spectrum[NFFT2 - 10..].iter())
78 .map(|c| c.norm_sqr())
79 .sum();
80 let total_energy: f32 = spectrum.iter().map(|c| c.norm_sqr()).sum();
81
82 assert!(total_energy > 0.0);
83 let frac = energy_near_dc / total_energy;
84 assert!(frac > 0.5, "energy near DC fraction = {frac:.3}");
85 }
86
87 #[test]
88 fn sine_offset_from_f0_not_at_dc() {
89 let f0 = 1000.0f32;
90 let audio: Vec<i16> = (0..NMAX)
91 .map(|n| {
92 let t = n as f32 / 12_000.0;
93 (10_000.0 * (2.0 * std::f32::consts::PI * (f0 + 100.0) * t).sin()) as i16
94 })
95 .collect();
96
97 let out = downsample_simple(&audio, f0);
98
99 let mut spectrum = out.clone();
100 let mut planner = rustfft::FftPlanner::<f32>::new();
101 planner.plan_fft_forward(NFFT2).process(&mut spectrum);
102
103 let energy_near_dc: f32 = spectrum[..=2].iter().map(|c| c.norm_sqr()).sum();
104 let total_energy: f32 = spectrum.iter().map(|c| c.norm_sqr()).sum();
105
106 let frac = energy_near_dc / total_energy;
107 assert!(frac < 0.1, "energy at DC fraction = {frac:.3}");
108 }
109
110 #[test]
111 fn output_length() {
112 let audio = vec![0i16; NMAX];
113 let out = downsample_simple(&audio, 1000.0);
114 assert_eq!(out.len(), NFFT2);
115 }
116
117 #[test]
118 fn silence_gives_zero_output() {
119 let audio = vec![0i16; NMAX];
120 let out = downsample_simple(&audio, 1500.0);
121 let max_abs = out.iter().map(|c| c.norm()).fold(0.0f32, f32::max);
122 assert!(max_abs < 1e-10);
123 }
124}