aether_timbre/
analysis.rs1use rustfft::{FftPlanner, num_complex::Complex};
4
5#[derive(Debug, Clone)]
8pub struct SpectralEnvelope {
9 pub magnitudes: Vec<f32>,
11 pub fft_size: usize,
13 pub sample_rate: f32,
15}
16
17impl SpectralEnvelope {
18 pub fn analyze(audio: &[f32], sample_rate: f32, fft_size: usize) -> Self {
20 let fft_size = fft_size.next_power_of_two();
21 let mut planner = FftPlanner::<f32>::new();
22 let fft = planner.plan_fft_forward(fft_size);
23
24 let hop = fft_size / 4;
26 let num_frames = (audio.len().saturating_sub(fft_size)) / hop + 1;
27 let _ = num_frames; let mut avg_magnitudes = vec![0.0f32; fft_size / 2 + 1];
29
30 let window: Vec<f32> = (0..fft_size)
31 .map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (fft_size - 1) as f32).cos()))
32 .collect();
33
34 let mut frame_count = 0;
35 let mut offset = 0;
36 while offset + fft_size <= audio.len() {
37 let mut buf: Vec<Complex<f32>> = audio[offset..offset + fft_size]
38 .iter()
39 .zip(window.iter())
40 .map(|(&s, &w)| Complex::new(s * w, 0.0))
41 .collect();
42
43 fft.process(&mut buf);
44
45 for (i, mag) in avg_magnitudes.iter_mut().enumerate() {
46 *mag += buf[i].norm();
47 }
48 frame_count += 1;
49 offset += hop;
50 }
51
52 if frame_count > 0 {
53 for m in avg_magnitudes.iter_mut() {
54 *m /= frame_count as f32;
55 }
56 }
57
58 let smoothed = smooth_envelope(&avg_magnitudes, 8);
60
61 Self {
62 magnitudes: smoothed,
63 fft_size,
64 sample_rate,
65 }
66 }
67
68 pub fn normalize(&mut self) {
70 let peak = self.magnitudes.iter().cloned().fold(0.0f32, f32::max);
71 if peak > 0.0 {
72 for m in self.magnitudes.iter_mut() {
73 *m /= peak;
74 }
75 }
76 }
77
78 pub fn magnitude_at_freq(&self, freq: f32) -> f32 {
80 let bin_width = self.sample_rate / self.fft_size as f32;
81 let bin = freq / bin_width;
82 let bin_floor = bin as usize;
83 let frac = bin - bin_floor as f32;
84 let m0 = self.magnitudes.get(bin_floor).copied().unwrap_or(0.0);
85 let m1 = self.magnitudes.get(bin_floor + 1).copied().unwrap_or(0.0);
86 m0 + (m1 - m0) * frac
87 }
88}
89
90fn smooth_envelope(mags: &[f32], window: usize) -> Vec<f32> {
91 let n = mags.len();
92 let mut out = vec![0.0f32; n];
93 for (i, val) in out.iter_mut().enumerate() {
94 let start = i.saturating_sub(window / 2);
95 let end = (i + window / 2 + 1).min(n);
96 let sum: f32 = mags[start..end].iter().sum();
97 *val = sum / (end - start) as f32;
98 }
99 out
100}
101
102#[derive(Debug, Clone)]
104pub struct TimbreProfile {
105 pub name: String,
106 pub description: String,
107 pub envelopes: Vec<(u8, SpectralEnvelope)>, pub sample_rate: f32,
110 pub fft_size: usize,
111}
112
113impl TimbreProfile {
114 pub fn new(name: &str, sample_rate: f32, fft_size: usize) -> Self {
115 Self {
116 name: name.into(),
117 description: String::new(),
118 envelopes: Vec::new(),
119 sample_rate,
120 fft_size,
121 }
122 }
123
124 pub fn add_envelope(&mut self, note: u8, audio: &[f32]) {
126 let mut env = SpectralEnvelope::analyze(audio, self.sample_rate, self.fft_size);
127 env.normalize();
128 self.envelopes.push((note, env));
129 }
130
131 pub fn envelope_for_note(&self, note: u8) -> Option<&SpectralEnvelope> {
133 if self.envelopes.is_empty() { return None; }
134 let mut best_dist = u8::MAX;
135 for (n, _env) in &self.envelopes {
136 let dist = note.abs_diff(*n);
137 if dist < best_dist {
138 best_dist = dist;
139 }
140 }
141 self.envelopes.iter()
143 .min_by_key(|(n, _)| note.abs_diff(*n))
144 .map(|(_, e)| e)
145 }
146}