1use super::tf_surface::TfSurface;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum SurfaceEditError {
27 BufferTooSmall { needed: usize, have: usize },
28 InvalidRegion,
29}
30
31impl core::fmt::Display for SurfaceEditError {
32 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33 match self {
34 Self::BufferTooSmall { needed, have } => {
35 write!(
36 f,
37 "surface edit: buffer too small, need {needed}, have {have}"
38 )
39 }
40 Self::InvalidRegion => write!(f, "surface edit: invalid region"),
41 }
42 }
43}
44
45impl std::error::Error for SurfaceEditError {}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Region {
54 pub frame_start: usize,
55 pub frame_end: usize,
56 pub bin_start: usize,
57 pub bin_end: usize,
58}
59
60impl Region {
61 #[inline]
62 pub fn new(frame_start: usize, frame_end: usize, bin_start: usize, bin_end: usize) -> Self {
63 Self {
64 frame_start,
65 frame_end,
66 bin_start,
67 bin_end,
68 }
69 }
70
71 #[inline]
72 pub fn full(frame_count: usize, bin_count: usize) -> Self {
73 Self::new(0, frame_count, 0, bin_count)
74 }
75
76 #[inline]
77 pub fn frame_span(&self) -> usize {
78 self.frame_end.saturating_sub(self.frame_start)
79 }
80
81 #[inline]
82 pub fn bin_span(&self) -> usize {
83 self.bin_end.saturating_sub(self.bin_start)
84 }
85
86 #[inline]
87 pub fn is_valid(&self) -> bool {
88 self.frame_end > self.frame_start && self.bin_end > self.bin_start
89 }
90}
91
92pub fn apply_gain(
99 surface: &TfSurface,
100 region: &Region,
101 gain: f32,
102 out: &mut [f32],
103) -> Result<usize, SurfaceEditError> {
104 let needed = surface.frame_count * surface.bin_count;
105 if out.len() < needed {
106 return Err(SurfaceEditError::BufferTooSmall {
107 needed,
108 have: out.len(),
109 });
110 }
111
112 out[..needed].copy_from_slice(surface.raster);
114
115 let f_end = region.frame_end.min(surface.frame_count);
116 let b_end = region.bin_end.min(surface.bin_count);
117 for f in region.frame_start..f_end {
118 for b in region.bin_start..b_end {
119 out[f * surface.bin_count + b] *= gain;
120 }
121 }
122
123 Ok(needed)
124}
125
126pub fn spectral_gate(
128 surface: &TfSurface,
129 region: &Region,
130 threshold: f32,
131 out: &mut [f32],
132) -> Result<usize, SurfaceEditError> {
133 let needed = surface.frame_count * surface.bin_count;
134 if out.len() < needed {
135 return Err(SurfaceEditError::BufferTooSmall {
136 needed,
137 have: out.len(),
138 });
139 }
140
141 out[..needed].copy_from_slice(surface.raster);
142
143 let f_end = region.frame_end.min(surface.frame_count);
144 let b_end = region.bin_end.min(surface.bin_count);
145 for f in region.frame_start..f_end {
146 for b in region.bin_start..b_end {
147 let idx = f * surface.bin_count + b;
148 if out[idx] < threshold {
149 out[idx] = 0.0;
150 }
151 }
152 }
153
154 Ok(needed)
155}
156
157pub fn copy_patch(
160 surface: &TfSurface,
161 src_region: &Region,
162 dst_frame: usize,
163 dst_bin: usize,
164 out: &mut [f32],
165) -> Result<usize, SurfaceEditError> {
166 let needed = surface.frame_count * surface.bin_count;
167 if out.len() < needed {
168 return Err(SurfaceEditError::BufferTooSmall {
169 needed,
170 have: out.len(),
171 });
172 }
173
174 out[..needed].copy_from_slice(surface.raster);
175
176 let f_span = src_region.frame_span();
177 let b_span = src_region.bin_span();
178
179 for df in 0..f_span {
180 let src_f = src_region.frame_start + df;
181 let dst_f = dst_frame + df;
182 if src_f >= surface.frame_count || dst_f >= surface.frame_count {
183 break;
184 }
185 for db in 0..b_span {
186 let src_b = src_region.bin_start + db;
187 let dst_b = dst_bin + db;
188 if src_b >= surface.bin_count || dst_b >= surface.bin_count {
189 break;
190 }
191 out[dst_f * surface.bin_count + dst_b] =
192 surface.raster[src_f * surface.bin_count + src_b];
193 }
194 }
195
196 Ok(needed)
197}
198
199pub fn time_stretch(
204 surface: &TfSurface,
205 factor: f32,
206 out: &mut [f32],
207) -> Result<(usize, usize), SurfaceEditError> {
208 let new_frames = (surface.frame_count as f32 * factor).round() as usize;
209 let new_frames = new_frames.max(1);
210 let needed = new_frames * surface.bin_count;
211 if out.len() < needed {
212 return Err(SurfaceEditError::BufferTooSmall {
213 needed,
214 have: out.len(),
215 });
216 }
217
218 for f in 0..new_frames {
219 let src_f = f as f32 / factor;
220 for b in 0..surface.bin_count {
221 out[f * surface.bin_count + b] = surface.sample_bilinear(src_f, b as f32);
222 }
223 }
224
225 Ok((new_frames, surface.bin_count))
226}
227
228pub fn pitch_shift(
232 surface: &TfSurface,
233 factor: f32,
234 out: &mut [f32],
235) -> Result<usize, SurfaceEditError> {
236 let needed = surface.frame_count * surface.bin_count;
237 if out.len() < needed {
238 return Err(SurfaceEditError::BufferTooSmall {
239 needed,
240 have: out.len(),
241 });
242 }
243
244 for f in 0..surface.frame_count {
245 for b in 0..surface.bin_count {
246 let src_b = b as f32 / factor;
247 out[f * surface.bin_count + b] = surface.sample_bilinear(f as f32, src_b);
248 }
249 }
250
251 Ok(needed)
252}
253
254pub fn crossfade(
257 surface_a: &TfSurface,
258 surface_b: &TfSurface,
259 t: f32,
260 out: &mut [f32],
261) -> Result<usize, SurfaceEditError> {
262 if surface_a.frame_count != surface_b.frame_count || surface_a.bin_count != surface_b.bin_count
263 {
264 return Err(SurfaceEditError::InvalidRegion);
265 }
266
267 let needed = surface_a.frame_count * surface_a.bin_count;
268 if out.len() < needed {
269 return Err(SurfaceEditError::BufferTooSmall {
270 needed,
271 have: out.len(),
272 });
273 }
274
275 for i in 0..needed {
276 out[i] = surface_a.raster[i] * (1.0 - t) + surface_b.raster[i] * t;
277 }
278
279 Ok(needed)
280}
281
282pub fn fade_in(
284 surface: &TfSurface,
285 fade_frames: usize,
286 out: &mut [f32],
287) -> Result<usize, SurfaceEditError> {
288 let needed = surface.frame_count * surface.bin_count;
289 if out.len() < needed {
290 return Err(SurfaceEditError::BufferTooSmall {
291 needed,
292 have: out.len(),
293 });
294 }
295
296 out[..needed].copy_from_slice(surface.raster);
297
298 let fade = fade_frames.min(surface.frame_count);
299 for f in 0..fade {
300 let gain = f as f32 / fade as f32;
301 for b in 0..surface.bin_count {
302 out[f * surface.bin_count + b] *= gain;
303 }
304 }
305
306 Ok(needed)
307}
308
309pub fn fade_out(
311 surface: &TfSurface,
312 fade_frames: usize,
313 out: &mut [f32],
314) -> Result<usize, SurfaceEditError> {
315 let needed = surface.frame_count * surface.bin_count;
316 if out.len() < needed {
317 return Err(SurfaceEditError::BufferTooSmall {
318 needed,
319 have: out.len(),
320 });
321 }
322
323 out[..needed].copy_from_slice(surface.raster);
324
325 let fade = fade_frames.min(surface.frame_count);
326 let start = surface.frame_count - fade;
327 for f in 0..fade {
328 let gain = 1.0 - (f + 1) as f32 / fade as f32;
329 for b in 0..surface.bin_count {
330 out[(start + f) * surface.bin_count + b] *= gain;
331 }
332 }
333
334 Ok(needed)
335}
336
337#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::audio::audio_spectral_sheet::SPECTRAL_PREVIEW_BINS;
345
346 fn make_surface() -> (Vec<f32>, usize, usize) {
347 let frames = 4;
348 let bins = SPECTRAL_PREVIEW_BINS;
349 let mut raster = vec![0.5f32; frames * bins];
350 raster[1 * bins + 16] = 1.0;
352 raster[2 * bins + 32] = 0.8;
353 (raster, frames, bins)
354 }
355
356 #[test]
357 fn gain_scales_region() {
358 let (raster, frames, bins) = make_surface();
359 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
360 let region = Region::new(1, 3, 10, 20);
361 let mut out = vec![0.0f32; frames * bins];
362 apply_gain(&s, ®ion, 2.0, &mut out).unwrap();
363 assert!(
365 (out[1 * bins + 16] - 2.0).abs() < 1e-6,
366 "peak should be doubled"
367 );
368 assert!(
370 (out[0 * bins + 0] - 0.5).abs() < 1e-6,
371 "outside should be unchanged"
372 );
373 }
374
375 #[test]
376 fn spectral_gate_zeros_below_threshold() {
377 let (raster, frames, bins) = make_surface();
378 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
379 let region = Region::full(frames, bins);
380 let mut out = vec![0.0f32; frames * bins];
381 spectral_gate(&s, ®ion, 0.6, &mut out).unwrap();
382 assert_eq!(out[0 * bins + 0], 0.0, "0.5 should be gated");
384 assert!((out[1 * bins + 16] - 1.0).abs() < 1e-6, "1.0 should remain");
386 assert!((out[2 * bins + 32] - 0.8).abs() < 1e-6, "0.8 should remain");
387 }
388
389 #[test]
390 fn copy_patch_translates_region() {
391 let (raster, frames, bins) = make_surface();
392 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
393 let src = Region::new(1, 2, 16, 17); let mut out = vec![0.0f32; frames * bins];
395 copy_patch(&s, &src, 3, 40, &mut out).unwrap();
396 assert!(
398 (out[3 * bins + 40] - 1.0).abs() < 1e-6,
399 "patch should be copied"
400 );
401 }
402
403 #[test]
404 fn time_stretch_doubles_frames() {
405 let (raster, frames, bins) = make_surface();
406 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
407 let mut out = vec![0.0f32; frames * 2 * bins];
408 let (new_frames, _) = time_stretch(&s, 2.0, &mut out).unwrap();
409 assert_eq!(new_frames, 8, "should double frame count");
410 }
411
412 #[test]
413 fn time_stretch_preserves_energy_pattern() {
414 let (raster, frames, bins) = make_surface();
415 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
416 let mut out = vec![0.0f32; frames * 2 * bins];
417 let (new_frames, _) = time_stretch(&s, 2.0, &mut out).unwrap();
418 let peak_frame = (0..new_frames)
420 .map(|f| (f, out[f * bins + 16]))
421 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
422 .unwrap()
423 .0;
424 assert!(
425 peak_frame >= 1 && peak_frame <= 3,
426 "peak should be near frame 2"
427 );
428 }
429
430 #[test]
431 fn pitch_shift_preserves_frame_count() {
432 let (raster, frames, bins) = make_surface();
433 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
434 let mut out = vec![0.0f32; frames * bins];
435 pitch_shift(&s, 2.0, &mut out).unwrap();
436 assert!(
439 out.iter().any(|&v| v > 0.0),
440 "pitch shift should preserve energy"
441 );
442 }
443
444 #[test]
445 fn crossfade_blends_surfaces() {
446 let (raster_a, frames, bins) = make_surface();
447 let raster_b = vec![1.0f32; frames * bins];
448 let sa = TfSurface::new(&raster_a, frames, bins, 44100, 512);
449 let sb = TfSurface::new(&raster_b, frames, bins, 44100, 512);
450 let mut out = vec![0.0f32; frames * bins];
451 crossfade(&sa, &sb, 0.5, &mut out).unwrap();
452 let expected = (raster_a[0] + raster_b[0]) * 0.5;
454 assert!((out[0] - expected).abs() < 1e-6, "crossfade should blend");
455 }
456
457 #[test]
458 fn crossfade_at_t0_returns_a() {
459 let (raster_a, frames, bins) = make_surface();
460 let raster_b = vec![1.0f32; frames * bins];
461 let sa = TfSurface::new(&raster_a, frames, bins, 44100, 512);
462 let sb = TfSurface::new(&raster_b, frames, bins, 44100, 512);
463 let mut out = vec![0.0f32; frames * bins];
464 crossfade(&sa, &sb, 0.0, &mut out).unwrap();
465 for i in 0..frames * bins {
466 assert!(
467 (out[i] - raster_a[i]).abs() < 1e-6,
468 "t=0 should return surface A"
469 );
470 }
471 }
472
473 #[test]
474 fn crossfade_at_t1_returns_b() {
475 let (raster_a, frames, bins) = make_surface();
476 let raster_b = vec![1.0f32; frames * bins];
477 let sa = TfSurface::new(&raster_a, frames, bins, 44100, 512);
478 let sb = TfSurface::new(&raster_b, frames, bins, 44100, 512);
479 let mut out = vec![0.0f32; frames * bins];
480 crossfade(&sa, &sb, 1.0, &mut out).unwrap();
481 for i in 0..frames * bins {
482 assert!(
483 (out[i] - raster_b[i]).abs() < 1e-6,
484 "t=1 should return surface B"
485 );
486 }
487 }
488
489 #[test]
490 fn fade_in_ramps_from_zero() {
491 let (raster, frames, bins) = make_surface();
492 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
493 let mut out = vec![0.0f32; frames * bins];
494 fade_in(&s, 2, &mut out).unwrap();
495 assert_eq!(out[0], 0.0, "frame 0 should be zeroed by fade-in");
497 assert!(
499 (out[1 * bins] - 0.25).abs() < 1e-6,
500 "frame 1 should be halved"
501 );
502 assert!(
504 (out[2 * bins] - 0.5).abs() < 1e-6,
505 "frame 2 should be unchanged"
506 );
507 }
508
509 #[test]
510 fn fade_out_ramps_to_zero() {
511 let (raster, frames, bins) = make_surface();
512 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
513 let mut out = vec![0.0f32; frames * bins];
514 fade_out(&s, 2, &mut out).unwrap();
515 assert_eq!(out[(frames - 1) * bins], 0.0, "last frame should be zeroed");
517 assert!(
519 (out[(frames - 2) * bins] - 0.25).abs() < 1e-6,
520 "second-to-last should be halved"
521 );
522 }
523
524 #[test]
525 fn all_edits_deterministic() {
526 let (raster, frames, bins) = make_surface();
527 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
528 let region = Region::new(0, 2, 0, 32);
529
530 let mut out1 = vec![0.0f32; frames * bins];
531 let mut out2 = vec![0.0f32; frames * bins];
532 apply_gain(&s, ®ion, 1.5, &mut out1).unwrap();
533 apply_gain(&s, ®ion, 1.5, &mut out2).unwrap();
534 assert_eq!(out1, out2, "gain must be deterministic");
535
536 let mut out3 = vec![0.0f32; frames * bins];
537 let mut out4 = vec![0.0f32; frames * bins];
538 spectral_gate(&s, ®ion, 0.3, &mut out3).unwrap();
539 spectral_gate(&s, ®ion, 0.3, &mut out4).unwrap();
540 assert_eq!(out3, out4, "gate must be deterministic");
541 }
542
543 #[test]
544 fn buffer_too_small_errors() {
545 let (raster, frames, bins) = make_surface();
546 let s = TfSurface::new(&raster, frames, bins, 44100, 512);
547 let mut out = vec![0.0f32; 10]; let region = Region::full(frames, bins);
549 let err = apply_gain(&s, ®ion, 2.0, &mut out).unwrap_err();
550 assert_eq!(
551 err,
552 SurfaceEditError::BufferTooSmall {
553 needed: frames * bins,
554 have: 10,
555 }
556 );
557 }
558}