1use rustfft::{FftPlanner, num_complex::Complex};
15
16#[derive(Clone, Copy, Debug)]
18pub enum Window { Hann, Hamming, Blackman, Rectangular }
19
20impl Window {
21 fn apply(self, buf: &mut [f32]) {
22 let n = buf.len();
23 for (i, s) in buf.iter_mut().enumerate() {
24 let w = match self {
25 Self::Hann =>
26 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (n - 1) as f32).cos()),
27 Self::Hamming =>
28 0.54 - 0.46 * (2.0 * std::f32::consts::PI * i as f32 / (n - 1) as f32).cos(),
29 Self::Blackman => {
30 let t = 2.0 * std::f32::consts::PI * i as f32 / (n - 1) as f32;
31 0.42 - 0.5 * t.cos() + 0.08 * (2.0 * t).cos()
32 }
33 Self::Rectangular => 1.0,
34 };
35 *s *= w;
36 }
37 }
38}
39
40pub struct FftAnalyzer {
42 fft_size: usize,
43 sample_rate: u32,
44 buffer: Vec<f32>,
45 window: Window,
46 pub magnitudes: Vec<f32>,
48 pub smoothed: Vec<f32>,
50 beat_energy: f32,
52 beat_avg: f32,
53 planner: FftPlanner<f32>,
54}
55
56impl FftAnalyzer {
57 pub fn new(fft_size: usize, sample_rate: u32) -> Self {
60 let bins = fft_size / 2;
61 Self {
62 fft_size,
63 sample_rate,
64 buffer: vec![0.0; fft_size],
65 window: Window::Hann,
66 magnitudes: vec![0.0; bins],
67 smoothed: vec![0.0; bins],
68 beat_energy: 0.0,
69 beat_avg: 0.001,
70 planner: FftPlanner::new(),
71 }
72 }
73
74 pub fn set_window(&mut self, w: Window) { self.window = w; }
75
76 pub fn push_samples(&mut self, samples: &[f32]) {
78 let n = samples.len().min(self.fft_size);
79 let shift = self.fft_size - n;
80 self.buffer.copy_within(n.., 0);
81 self.buffer[shift..].copy_from_slice(&samples[..n]);
82 self.compute();
83 }
84
85 fn compute(&mut self) {
86 let fft = self.planner.plan_fft_forward(self.fft_size);
87 let mut buf: Vec<Complex<f32>> = self.buffer.iter()
88 .cloned()
89 .map(|s| Complex { re: s, im: 0.0 })
90 .collect();
91
92 {
94 let mut real: Vec<f32> = buf.iter().map(|c| c.re).collect();
95 self.window.apply(&mut real);
96 for (c, &r) in buf.iter_mut().zip(real.iter()) { c.re = r; }
97 }
98
99 fft.process(&mut buf);
100
101 let scale = 2.0 / self.fft_size as f32;
103 let mut energy = 0.0f32;
104 for (i, mag) in self.magnitudes.iter_mut().enumerate() {
105 let m = buf[i].norm() * scale;
106 *mag = m;
107 energy += m * m;
108 }
109
110 let smooth = 0.82;
112 for (s, &m) in self.smoothed.iter_mut().zip(self.magnitudes.iter()) {
113 *s = *s * smooth + m * (1.0 - smooth);
114 }
115
116 self.beat_energy = energy / self.magnitudes.len() as f32;
118 self.beat_avg = self.beat_avg * 0.992 + self.beat_energy * 0.008;
119 }
120
121 pub fn freq_bands(&self, bands: usize) -> Vec<f32> {
126 if bands == 0 { return vec![]; }
127 let nyq = self.sample_rate as f32 / 2.0;
128 let bins = self.magnitudes.len();
129 let f_min = 20.0f32;
130 let f_max = nyq;
131 let log_lo = f_min.log2();
132 let log_hi = f_max.log2();
133
134 (0..bands).map(|b| {
135 let lo = 2.0f32.powf(log_lo + (b as f32 / bands as f32) * (log_hi - log_lo));
136 let hi = 2.0f32.powf(log_lo + ((b + 1) as f32 / bands as f32) * (log_hi - log_lo));
137 let bin_lo = ((lo / nyq) * bins as f32) as usize;
138 let bin_hi = ((hi / nyq) * bins as f32) as usize + 1;
139 let bin_lo = bin_lo.min(bins - 1);
140 let bin_hi = bin_hi.min(bins);
141 if bin_hi <= bin_lo {
142 self.smoothed[bin_lo]
143 } else {
144 self.smoothed[bin_lo..bin_hi].iter().cloned().fold(0.0f32, f32::max)
145 }
146 }).collect()
147 }
148
149 pub fn dominant_freq(&self) -> f32 {
151 let nyq = self.sample_rate as f32 / 2.0;
152 let bins = self.magnitudes.len();
153 let peak_bin = self.magnitudes.iter()
154 .enumerate()
155 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
156 .map(|(i, _)| i)
157 .unwrap_or(0);
158 peak_bin as f32 / bins as f32 * nyq
159 }
160
161 pub fn rms(&self) -> f32 {
163 (self.buffer.iter().map(|s| s * s).sum::<f32>() / self.fft_size as f32).sqrt()
164 }
165
166 pub fn is_beat(&self) -> bool {
168 self.beat_energy > self.beat_avg * 1.5
169 }
170
171 pub fn beat_ratio(&self) -> f32 {
173 self.beat_energy / self.beat_avg.max(1e-6)
174 }
175}
176
177#[derive(Clone, Debug)]
181pub struct CosPalette {
182 pub a: [f32; 3], pub b: [f32; 3], pub c: [f32; 3], pub d: [f32; 3], }
187
188impl CosPalette {
189 pub fn rainbow() -> Self {
191 Self {
192 a: [0.5, 0.5, 0.5],
193 b: [0.5, 0.5, 0.5],
194 c: [1.0, 1.0, 1.0],
195 d: [0.0, 0.333, 0.667],
196 }
197 }
198
199 pub fn fire() -> Self {
201 Self {
202 a: [0.8, 0.4, 0.1],
203 b: [0.7, 0.3, 0.1],
204 c: [1.0, 0.5, 0.3],
205 d: [0.0, 0.5, 0.8],
206 }
207 }
208
209 pub fn ocean() -> Self {
211 Self {
212 a: [0.1, 0.4, 0.7],
213 b: [0.3, 0.3, 0.4],
214 c: [0.8, 1.0, 0.5],
215 d: [0.3, 0.0, 0.6],
216 }
217 }
218
219 pub fn psychedelic() -> Self {
221 Self {
222 a: [0.5, 0.5, 0.5],
223 b: [0.8, 0.8, 0.8],
224 c: [1.0, 1.3, 0.7],
225 d: [0.0, 0.15, 0.3],
226 }
227 }
228
229 pub fn frequency() -> Self {
231 Self {
232 a: [0.5, 0.4, 0.5],
233 b: [0.5, 0.3, 0.5],
234 c: [0.5, 0.5, 1.0],
235 d: [0.0, 0.33, 0.67],
236 }
237 }
238
239 pub fn eval(&self, t: f32) -> [f32; 3] {
241 [0, 1, 2].map(|i| {
242 let v = self.a[i] + self.b[i] * (2.0 * std::f32::consts::PI * (self.c[i] * t + self.d[i])).cos();
243 v.clamp(0.0, 1.0)
244 })
245 }
246
247 pub fn eval_animated(&self, t: f32, time: f32, speed: f32) -> [f32; 3] {
249 self.eval((t + time * speed) % 1.0)
250 }
251
252 pub fn map_bands(&self, magnitudes: &[f32], time: f32, speed: f32) -> Vec<[u8; 4]> {
255 magnitudes.iter().enumerate().map(|(i, &m)| {
256 let t = i as f32 / magnitudes.len() as f32;
257 let [r, g, b] = self.eval_animated(t, time, speed);
258 let brightness = m.clamp(0.0, 1.0);
259 [(r * brightness * 255.0) as u8, (g * brightness * 255.0) as u8, (b * brightness * 255.0) as u8, (brightness * 255.0) as u8]
260 }).collect()
261 }
262}
263
264pub fn freq_texture(bands: &[f32], palette: &CosPalette, width: u32, height: u32, time: f32) -> Vec<u8> {
267 let w = width as usize;
268 let h = height as usize;
269 let mut pixels = vec![0u8; w * h * 4];
270 for x in 0..w {
271 let band_idx = (x * bands.len() / w).min(bands.len() - 1);
272 let mag = bands[band_idx].clamp(0.0, 1.0);
273 let fill_h = (mag * h as f32) as usize;
274 let t = x as f32 / w as f32;
275 let [r, g, b] = palette.eval_animated(t, time, 0.3);
276 for y in (h - fill_h)..h {
277 let alpha = (mag * (1.0 - (y as f32 / h as f32)) * 1.5).clamp(0.0, 1.0);
278 let idx = (y * w + x) * 4;
279 pixels[idx ] = (r * 255.0) as u8;
280 pixels[idx + 1] = (g * 255.0) as u8;
281 pixels[idx + 2] = (b * 255.0) as u8;
282 pixels[idx + 3] = (alpha * 255.0) as u8;
283 }
284 }
285 pixels
286}
287
288pub fn waveform_texture(samples: &[f32], palette: &CosPalette, width: u32, height: u32, time: f32) -> Vec<u8> {
290 let w = width as usize;
291 let h = height as usize;
292 let mut pixels = vec![0u8; w * h * 4];
293 for (i, &s) in samples.iter().enumerate() {
294 let x = i * w / samples.len();
295 let y = ((s * 0.5 + 0.5) * h as f32).clamp(0.0, h as f32 - 1.0) as usize;
296 let t = i as f32 / samples.len() as f32;
297 let [r, g, b] = palette.eval_animated(t, time, 0.2);
298 let idx = (y * w + x) * 4;
299 pixels[idx ] = (r * 255.0) as u8;
300 pixels[idx + 1] = (g * 255.0) as u8;
301 pixels[idx + 2] = (b * 255.0) as u8;
302 pixels[idx + 3] = 255;
303 }
304 pixels
305}