1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// FFT logic for time-series data, extracted from main.rs
// Provides windowing and spectrum calculation utilities for plotting
use rustfft::{num_complex::Complex, FftPlanner};
use std::collections::VecDeque;
/// Supported FFT window functions for spectral analysis.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FFTWindow {
/// Rectangular (no windowing)
Rect,
/// Hann window
Hann,
/// Hamming window
Hamming,
/// Blackman window
Blackman,
}
impl FFTWindow {
/// All available window types (for UI selection)
pub const ALL: &'static [FFTWindow] = &[
FFTWindow::Rect,
FFTWindow::Hann,
FFTWindow::Hamming,
FFTWindow::Blackman,
];
/// Human-readable label for each window type
pub fn label(&self) -> &'static str {
match self {
FFTWindow::Rect => "Rect",
FFTWindow::Hann => "Hann",
FFTWindow::Hamming => "Hamming",
FFTWindow::Blackman => "Blackman",
}
}
/// Compute the window weight for a given sample index
pub fn weight(&self, n: usize, len: usize) -> f64 {
match self {
FFTWindow::Rect => 1.0,
FFTWindow::Hann => {
// Hann window: w[n] = 0.5 - 0.5*cos(2*pi*n/(N-1))
0.5 - 0.5 * (2.0 * std::f64::consts::PI * n as f64 / (len as f64)).cos()
}
FFTWindow::Hamming => {
// Hamming window: w[n] = 0.54 - 0.46*cos(2*pi*n/(N-1))
0.54 - 0.46 * (2.0 * std::f64::consts::PI * n as f64 / (len as f64)).cos()
}
FFTWindow::Blackman => {
// Blackman window: w[n] = 0.42 - 0.5*cos(2*pi*n/(N-1)) + 0.08*cos(4*pi*n/(N-1))
0.42 - 0.5 * (2.0 * std::f64::consts::PI * n as f64 / (len as f64)).cos()
+ 0.08 * (4.0 * std::f64::consts::PI * n as f64 / (len as f64)).cos()
}
}
}
}
/// Compute the FFT of the most recent samples in the buffer, using the selected window.
///
/// - `buf`: The live buffer of [time, value] samples.
/// - `paused`: Whether the app is paused (if so, use snapshot buffer).
/// - `buffer_snapshot`: Optional snapshot buffer (used if paused).
/// - `fft_size`: Number of samples to use for FFT (must be <= buffer length).
/// - `fft_window`: Window function to apply before FFT.
///
/// Returns: `Some(Vec<[frequency, magnitude]>)` if enough data, else `None`.
pub fn compute_fft(
buf: &VecDeque<[f64; 2]>,
paused: bool,
buffer_snapshot: &Option<VecDeque<[f64; 2]>>,
fft_size: usize,
fft_window: FFTWindow,
) -> Option<Vec<[f64; 2]>> {
// Use snapshot buffer if paused, else live buffer
let buf = if paused {
buffer_snapshot.as_ref()?
} else {
buf
};
if buf.len() < fft_size {
return None;
}
let len = buf.len();
// Take the last fft_size samples for analysis
let slice: Vec<[f64; 2]> = buf.iter().skip(len - fft_size).cloned().collect();
if slice.len() != fft_size {
return None;
}
let t0 = slice.first()?[0];
let t1 = slice.last()?[0];
if !(t1 > t0) {
return None;
}
// Estimate sample rate from time axis
let dt_est = (t1 - t0) / (fft_size as f64 - 1.0);
if dt_est <= 0.0 {
return None;
}
let sample_rate = 1.0 / dt_est;
// Prepare windowed real input for FFT
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(fft_size);
let mut data: Vec<Complex<f64>> = slice
.iter()
.enumerate()
.map(|(i, arr)| {
let w = fft_window.weight(i, fft_size);
Complex {
re: arr[1] * w,
im: 0.0,
}
})
.collect();
fft.process(&mut data);
// Compute one-sided magnitude spectrum (up to Nyquist)
let half = fft_size / 2;
let scale = 2.0 / fft_size as f64; // amplitude normalization
let mut out: Vec<[f64; 2]> = Vec::with_capacity(half);
for (k, c) in data.iter().take(half).enumerate() {
let freq = k as f64 * sample_rate / fft_size as f64;
let mag = (c.re * c.re + c.im * c.im).sqrt() * scale;
out.push([freq, mag]);
}
Some(out)
}