1#[derive(Debug, Clone, Copy)]
30pub struct TfSurface<'a> {
31 pub raster: &'a [f32],
32 pub frame_count: usize,
33 pub bin_count: usize,
34 pub sample_rate: u32,
35 pub hop_size: usize,
36}
37
38impl<'a> TfSurface<'a> {
39 pub fn new(
43 raster: &'a [f32],
44 frame_count: usize,
45 bin_count: usize,
46 sample_rate: u32,
47 hop_size: usize,
48 ) -> Self {
49 debug_assert_eq!(raster.len(), frame_count * bin_count);
50 Self {
51 raster,
52 frame_count,
53 bin_count,
54 sample_rate,
55 hop_size,
56 }
57 }
58
59 #[inline]
61 pub fn get(&self, frame: usize, bin: usize) -> f32 {
62 if frame >= self.frame_count || bin >= self.bin_count {
63 return 0.0;
64 }
65 self.raster[frame * self.bin_count + bin]
66 }
67
68 #[inline]
70 pub fn frame_to_time(&self, frame: usize) -> f32 {
71 if self.sample_rate == 0 || self.hop_size == 0 {
72 return 0.0;
73 }
74 frame as f32 * self.hop_size as f32 / self.sample_rate as f32
75 }
76
77 #[inline]
79 pub fn bin_to_freq_linear(&self, bin: usize) -> f32 {
80 if self.sample_rate == 0 || self.bin_count == 0 {
81 return 0.0;
82 }
83 bin as f32 * (self.sample_rate as f32 / (2.0 * self.bin_count as f32))
84 }
85
86 #[inline]
89 pub fn bin_to_freq_log(&self, bin: usize, f_min: f32, bins_per_octave: usize) -> f32 {
90 if bins_per_octave == 0 {
91 return 0.0;
92 }
93 f_min * 2.0_f32.powf(bin as f32 / bins_per_octave as f32)
94 }
95
96 #[inline]
98 pub fn sample_bilinear(&self, frame_f: f32, bin_f: f32) -> f32 {
99 let f0 = frame_f.floor() as isize;
100 let b0 = bin_f.floor() as isize;
101 let df = frame_f - f0 as f32;
102 let db = bin_f - b0 as f32;
103
104 let f0 = f0.max(0).min(self.frame_count as isize - 1) as usize;
105 let f1 = (f0 + 1).min(self.frame_count - 1);
106 let b0 = b0.max(0).min(self.bin_count as isize - 1) as usize;
107 let b1 = (b0 + 1).min(self.bin_count - 1);
108
109 let v00 = self.get(f0, b0);
110 let v01 = self.get(f0, b1);
111 let v10 = self.get(f1, b0);
112 let v11 = self.get(f1, b1);
113
114 let top = v00 * (1.0 - db) + v01 * db;
115 let bot = v10 * (1.0 - db) + v11 * db;
116 top * (1.0 - df) + bot * df
117 }
118
119 pub fn spectral_flux(&self, out: &mut [f32]) -> usize {
122 let n = self.frame_count.saturating_sub(1);
123 if out.len() < n {
124 return 0;
125 }
126 for f in 0..n {
127 let mut flux = 0.0f32;
128 for b in 0..self.bin_count {
129 let diff = self.get(f + 1, b) - self.get(f, b);
130 if diff > 0.0 {
131 flux += diff * diff;
132 }
133 }
134 out[f] = flux.sqrt();
135 }
136 n
137 }
138
139 #[inline]
141 pub fn freq_gradient(&self, frame: usize, bin: usize) -> f32 {
142 let prev = self.get(frame, bin.saturating_sub(1));
143 let next = self.get(frame, (bin + 1).min(self.bin_count - 1));
144 (next - prev) * 0.5
145 }
146
147 #[inline]
149 pub fn time_gradient(&self, frame: usize, bin: usize) -> f32 {
150 let prev = self.get(frame.saturating_sub(1), bin);
151 let next = self.get((frame + 1).min(self.frame_count - 1), bin);
152 (next - prev) * 0.5
153 }
154
155 pub fn ridge_bin(&self, frame: usize) -> usize {
158 if frame >= self.frame_count {
159 return 0;
160 }
161 let row = &self.raster[frame * self.bin_count..(frame + 1) * self.bin_count];
162 let (idx, _) = row
163 .iter()
164 .enumerate()
165 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(core::cmp::Ordering::Equal))
166 .unwrap_or((0, &0.0));
167 idx
168 }
169
170 pub fn ridges(&self, out: &mut [usize]) -> usize {
173 let n = self.frame_count.min(out.len());
174 for f in 0..n {
175 out[f] = self.ridge_bin(f);
176 }
177 n
178 }
179
180 pub fn to_height_mesh(&self, out_verts: &mut [f32]) -> usize {
185 let n = self.frame_count * self.bin_count;
186 if out_verts.len() < n * 3 {
187 return 0;
188 }
189 for f in 0..self.frame_count {
190 let t = self.frame_to_time(f);
191 for b in 0..self.bin_count {
192 let freq = self.bin_to_freq_linear(b);
193 let mag = self.get(f, b);
194 let idx = (f * self.bin_count + b) * 3;
195 out_verts[idx] = t;
196 out_verts[idx + 1] = freq;
197 out_verts[idx + 2] = mag;
198 }
199 }
200 n
201 }
202
203 pub fn total_energy(&self) -> f32 {
205 self.raster.iter().copied().sum()
206 }
207
208 pub fn frame_energy(&self, frame: usize) -> f32 {
210 if frame >= self.frame_count {
211 return 0.0;
212 }
213 self.raster[frame * self.bin_count..(frame + 1) * self.bin_count]
214 .iter()
215 .copied()
216 .sum()
217 }
218}
219
220#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::audio::audio_spectral_sheet::SPECTRAL_PREVIEW_BINS;
228
229 fn make_test_surface() -> (Vec<f32>, usize, usize) {
230 let frames = 4;
231 let bins = SPECTRAL_PREVIEW_BINS;
232 let mut raster = vec![0.0f32; frames * bins];
233 raster[1 * bins + 16] = 1.0;
235 raster[2 * bins + 32] = 0.8;
237 (raster, frames, bins)
238 }
239
240 #[test]
241 fn surface_get_in_bounds() {
242 let (raster, frames, bins) = make_test_surface();
243 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
244 assert_eq!(s.get(1, 16), 1.0);
245 assert_eq!(s.get(2, 32), 0.8);
246 assert_eq!(s.get(0, 0), 0.0);
247 }
248
249 #[test]
250 fn surface_get_out_of_bounds_returns_zero() {
251 let (raster, frames, bins) = make_test_surface();
252 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
253 assert_eq!(s.get(100, 0), 0.0);
254 assert_eq!(s.get(0, 100), 0.0);
255 }
256
257 #[test]
258 fn bilinear_sample_at_integer_coords() {
259 let (raster, frames, bins) = make_test_surface();
260 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
261 assert!((s.sample_bilinear(1.0, 16.0) - 1.0).abs() < 1e-6);
262 assert!((s.sample_bilinear(2.0, 32.0) - 0.8).abs() < 1e-6);
263 }
264
265 #[test]
266 fn bilinear_sample_interpolates() {
267 let (raster, frames, bins) = make_test_surface();
268 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
269 let val = s.sample_bilinear(1.5, 24.0);
271 assert!(val >= 0.0, "bilinear sample should be non-negative");
273 }
274
275 #[test]
276 fn spectral_flux_detects_onsets() {
277 let (raster, frames, bins) = make_test_surface();
278 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
279 let mut flux = [0.0f32; 3];
280 let n = s.spectral_flux(&mut flux);
281 assert_eq!(n, 3);
282 assert!(flux[0] > 0.0, "flux at onset should be positive");
284 assert!(flux[1] > 0.0, "flux during peak move should be positive");
286 }
287
288 #[test]
289 fn ridge_bin_finds_peak() {
290 let (raster, frames, bins) = make_test_surface();
291 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
292 assert_eq!(s.ridge_bin(1), 16, "peak at frame 1 should be bin 16");
293 assert_eq!(s.ridge_bin(2), 32, "peak at frame 2 should be bin 32");
294 }
295
296 #[test]
297 fn ridges_collects_all_frames() {
298 let (raster, frames, bins) = make_test_surface();
299 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
300 let mut ridges = [0usize; 4];
301 let n = s.ridges(&mut ridges);
302 assert_eq!(n, 4);
303 assert_eq!(ridges[1], 16);
304 assert_eq!(ridges[2], 32);
305 }
306
307 #[test]
308 fn frame_to_time_correct() {
309 let (raster, frames, bins) = make_test_surface();
310 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
311 let t0 = s.frame_to_time(0);
312 let t1 = s.frame_to_time(1);
313 assert!((t0 - 0.0).abs() < 1e-6);
314 assert!((t1 - 512.0 / 44100.0).abs() < 1e-6);
315 }
316
317 #[test]
318 fn bin_to_freq_linear_correct() {
319 let (raster, frames, bins) = make_test_surface();
320 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
321 let f0 = s.bin_to_freq_linear(0);
323 let f1 = s.bin_to_freq_linear(1);
324 assert!((f0 - 0.0).abs() < 1e-6);
325 assert!(f1 > 0.0);
326 }
327
328 #[test]
329 fn bin_to_freq_log_correct() {
330 let (raster, frames, bins) = make_test_surface();
331 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
332 let f0 = s.bin_to_freq_log(0, 55.0, 12);
333 let f12 = s.bin_to_freq_log(12, 55.0, 12);
334 assert!((f0 - 55.0).abs() < 1e-3, "bin 0 should be f_min");
335 assert!((f12 - 110.0).abs() < 0.1, "bin 12 should be one octave up");
336 }
337
338 #[test]
339 fn freq_gradient_central_difference() {
340 let (raster, frames, bins) = make_test_surface();
341 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
342 let g = s.freq_gradient(1, 15);
344 assert!(g > 0.0, "gradient should be positive toward peak");
345 }
346
347 #[test]
348 fn time_gradient_central_difference() {
349 let frames = 4;
351 let bins = SPECTRAL_PREVIEW_BINS;
352 let mut raster = vec![0.0f32; frames * bins];
353 raster[1 * bins + 16] = 1.0; let s = TfSurface::new(&raster, frames, bins, 44100, 512);
355 let g = s.time_gradient(2, 16);
360 assert!(
361 g < 0.0,
362 "time gradient should be negative after peak: {}",
363 g
364 );
365 }
366
367 #[test]
368 fn total_energy_correct() {
369 let (raster, frames, bins) = make_test_surface();
370 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
371 assert!(
372 (s.total_energy() - 1.8).abs() < 1e-6,
373 "total energy should be 1.8"
374 );
375 }
376
377 #[test]
378 fn frame_energy_correct() {
379 let (raster, frames, bins) = make_test_surface();
380 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
381 assert!((s.frame_energy(1) - 1.0).abs() < 1e-6);
382 assert!((s.frame_energy(2) - 0.8).abs() < 1e-6);
383 assert!((s.frame_energy(0) - 0.0).abs() < 1e-6);
384 }
385
386 #[test]
387 fn to_height_mesh_writes_vertices() {
388 let (raster, frames, bins) = make_test_surface();
389 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
390 let mut verts = vec![0.0f32; frames * bins * 3];
391 let n = s.to_height_mesh(&mut verts);
392 assert_eq!(n, frames * bins);
393 let idx = (1 * bins + 16) * 3;
395 assert!((verts[idx + 2] - 1.0).abs() < 1e-6, "z should be magnitude");
396 }
397
398 #[test]
399 fn surface_determinism() {
400 let (raster, frames, bins) = make_test_surface();
401 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
402 let v1 = s.sample_bilinear(1.5, 24.0);
403 let v2 = s.sample_bilinear(1.5, 24.0);
404 assert_eq!(v1, v2, "sampling must be deterministic");
405 }
406}