Skip to main content

piper_plus/
audio_format.rs

1//! Audio format conversion and resampling utilities.
2//!
3//! Provides sample rate conversion, format conversion, and audio processing.
4//! Resampling uses the `rubato` crate (feature-gated behind "resample").
5
6#[cfg(feature = "resample")]
7use crate::error::PiperError;
8
9/// Audio format specification
10#[derive(Debug, Clone, PartialEq)]
11pub struct AudioFormat {
12    pub sample_rate: u32,
13    pub channels: u16,
14    pub bits_per_sample: u16,
15}
16
17impl AudioFormat {
18    pub fn mono_16bit(sample_rate: u32) -> Self {
19        Self {
20            sample_rate,
21            channels: 1,
22            bits_per_sample: 16,
23        }
24    }
25
26    /// Piper default: 22050 Hz, mono, 16-bit
27    pub fn piper_default() -> Self {
28        Self::mono_16bit(22050)
29    }
30}
31
32/// Resample audio from one sample rate to another.
33/// Uses linear interpolation (always available, no external deps).
34pub fn resample_linear(samples: &[i16], from_rate: u32, to_rate: u32) -> Vec<i16> {
35    if samples.is_empty() || from_rate == 0 || to_rate == 0 {
36        return Vec::new();
37    }
38
39    if from_rate == to_rate {
40        return samples.to_vec();
41    }
42
43    let ratio = to_rate as f64 / from_rate as f64;
44    let out_len = (samples.len() as f64 * ratio).ceil() as usize;
45    let mut output = Vec::with_capacity(out_len);
46
47    let step = from_rate as f64 / to_rate as f64; // pre-computed inverse ratio
48    let mut src_pos = 0.0_f64;
49    for _i in 0..out_len {
50        let src_idx = src_pos as usize;
51        let frac = src_pos - src_idx as f64;
52
53        let sample = if src_idx + 1 < samples.len() {
54            let a = samples[src_idx] as f64;
55            let b = samples[src_idx + 1] as f64;
56            (a + (b - a) * frac) as i16
57        } else {
58            // Last sample: no interpolation partner, use as-is
59            samples[samples.len() - 1]
60        };
61
62        output.push(sample);
63        src_pos += step; // addition instead of division per sample
64    }
65
66    output
67}
68
69/// High-quality resampling using rubato (feature-gated).
70/// Uses sinc interpolation for better quality.
71#[cfg(feature = "resample")]
72pub fn resample_sinc(
73    samples: &[i16],
74    from_rate: u32,
75    to_rate: u32,
76) -> Result<Vec<i16>, PiperError> {
77    use rubato::{
78        Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
79    };
80
81    if samples.is_empty() || from_rate == 0 || to_rate == 0 {
82        return Ok(Vec::new());
83    }
84
85    if from_rate == to_rate {
86        return Ok(samples.to_vec());
87    }
88
89    let params = SincInterpolationParameters {
90        sinc_len: 256,
91        f_cutoff: 0.95,
92        interpolation: SincInterpolationType::Linear,
93        oversampling_factor: 256,
94        window: WindowFunction::BlackmanHarris2,
95    };
96
97    let ratio = to_rate as f64 / from_rate as f64;
98    let chunk_size = 1024;
99
100    let mut resampler = SincFixedIn::<f64>::new(
101        ratio, 2.0, params, chunk_size, 1, // mono
102    )
103    .map_err(|e| PiperError::Inference(format!("resample init failed: {e}")))?;
104
105    // Convert i16 -> f64
106    let input_f64: Vec<f64> = samples.iter().map(|&s| s as f64 / 32768.0).collect();
107
108    let mut output_f64 = Vec::new();
109
110    // Process in chunks
111    let mut pos = 0;
112    while pos + chunk_size <= input_f64.len() {
113        let chunk = &input_f64[pos..pos + chunk_size];
114        let result = resampler
115            .process(&[chunk], None)
116            .map_err(|e| PiperError::Inference(format!("resample failed: {e}")))?;
117        output_f64.extend_from_slice(&result[0]);
118        pos += chunk_size;
119    }
120
121    // Process remaining samples (pad with zeros if needed)
122    if pos < input_f64.len() {
123        let remaining = &input_f64[pos..];
124        let mut padded = remaining.to_vec();
125        padded.resize(chunk_size, 0.0);
126        let result = resampler
127            .process(&[&padded], None)
128            .map_err(|e| PiperError::Inference(format!("resample failed: {e}")))?;
129        // Only take proportional output
130        let expected = ((input_f64.len() - pos) as f64 * ratio).ceil() as usize;
131        let take = expected.min(result[0].len());
132        output_f64.extend_from_slice(&result[0][..take]);
133    }
134
135    // Convert f64 -> i16
136    let output: Vec<i16> = output_f64
137        .iter()
138        .map(|&s| (s * 32767.0).clamp(-32768.0, 32767.0) as i16)
139        .collect();
140
141    Ok(output)
142}
143
144/// Convert mono to stereo (duplicate channel)
145pub fn mono_to_stereo(samples: &[i16]) -> Vec<i16> {
146    let mut output = Vec::with_capacity(samples.len() * 2);
147    for &s in samples {
148        output.push(s);
149        output.push(s);
150    }
151    output
152}
153
154/// Convert stereo to mono (average channels)
155pub fn stereo_to_mono(samples: &[i16]) -> Vec<i16> {
156    samples
157        .chunks_exact(2)
158        .map(|pair| {
159            // Use i32 to avoid overflow when averaging
160            ((pair[0] as i32 + pair[1] as i32) / 2) as i16
161        })
162        .collect()
163}
164
165/// Convert i16 samples to f32 (-1.0 to 1.0)
166pub fn i16_to_f32(samples: &[i16]) -> Vec<f32> {
167    samples.iter().map(|&s| s as f32 / 32768.0).collect()
168}
169
170/// Convert f32 samples to i16 (with clamping)
171pub fn f32_to_i16(samples: &[f32]) -> Vec<i16> {
172    samples
173        .iter()
174        .map(|&s| (s * 32768.0).clamp(-32768.0, 32767.0) as i16)
175        .collect()
176}
177
178/// Normalize audio to a target peak level (in dB, e.g., -1.0 dB).
179///
180/// `target_db` is relative to full scale (0 dB = i16::MAX).
181/// A value of -1.0 means the peak will be at ~91.2% of full scale.
182pub fn normalize_peak(samples: &mut [i16], target_db: f32) {
183    if samples.is_empty() {
184        return;
185    }
186
187    // Find current peak
188    let current_peak = samples
189        .iter()
190        .map(|&s| (s as i32).unsigned_abs())
191        .max()
192        .unwrap_or(0);
193
194    if current_peak == 0 {
195        return; // Silence -- nothing to normalize
196    }
197
198    // Target peak in linear scale (0 dB = 32767)
199    let target_linear = 32767.0_f64 * 10.0_f64.powf(target_db as f64 / 20.0);
200    let scale = target_linear / current_peak as f64;
201
202    for s in samples.iter_mut() {
203        *s = (*s as f64 * scale).clamp(-32768.0, 32767.0) as i16;
204    }
205}
206
207/// Compute RMS level in dB.
208///
209/// Returns the RMS relative to full scale (0 dB = 32768).
210/// Returns `f32::NEG_INFINITY` for silence (all zeros).
211pub fn rms_db(samples: &[i16]) -> f32 {
212    if samples.is_empty() {
213        return f32::NEG_INFINITY;
214    }
215
216    let sum_sq: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
217    let rms = (sum_sq / samples.len() as f64).sqrt();
218
219    if rms == 0.0 {
220        return f32::NEG_INFINITY;
221    }
222
223    (20.0 * (rms / 32768.0).log10()) as f32
224}
225
226/// Trim silence from the beginning and end of audio.
227///
228/// Removes leading and trailing samples whose absolute value falls below
229/// the given threshold (in dB relative to full scale).
230pub fn trim_silence(samples: &[i16], threshold_db: f32) -> &[i16] {
231    if samples.is_empty() {
232        return samples;
233    }
234
235    // Convert threshold from dB to linear amplitude
236    let threshold_linear = (32768.0_f64 * 10.0_f64.powf(threshold_db as f64 / 20.0)) as i32;
237
238    // Find first sample above threshold
239    let start = samples
240        .iter()
241        .position(|&s| (s as i32).abs() >= threshold_linear)
242        .unwrap_or(0);
243
244    // Find last sample above threshold
245    let end = samples
246        .iter()
247        .rposition(|&s| (s as i32).abs() >= threshold_linear)
248        .map(|p| p + 1)
249        .unwrap_or(0);
250
251    if start >= end {
252        return &samples[0..0];
253    }
254
255    &samples[start..end]
256}
257
258/// Apply a simple fade-in effect (linear amplitude ramp).
259pub fn fade_in(samples: &mut [i16], fade_samples: usize) {
260    let fade_len = fade_samples.min(samples.len());
261    if fade_len == 0 {
262        return;
263    }
264    let inv_len = 1.0_f64 / fade_len as f64; // compute once
265    let mut gain = 0.0_f64;
266    for s in samples[..fade_len].iter_mut() {
267        *s = (*s as f64 * gain) as i16;
268        gain += inv_len; // addition per sample instead of division
269    }
270}
271
272/// Apply a simple fade-out effect (linear amplitude ramp).
273pub fn fade_out(samples: &mut [i16], fade_samples: usize) {
274    let len = samples.len();
275    let fade_len = fade_samples.min(len);
276    if fade_len == 0 {
277        return;
278    }
279    let fade_start = len - fade_len;
280    let inv_len = 1.0_f64 / fade_len as f64; // compute once
281    let mut gain = 1.0_f64;
282    for s in samples[fade_start..].iter_mut() {
283        *s = (*s as f64 * gain) as i16;
284        gain -= inv_len; // addition per sample instead of division
285    }
286}
287
288/// Concatenate multiple audio chunks with optional crossfade.
289///
290/// When `crossfade_samples` is 0, performs simple concatenation.
291/// When > 0, applies linear crossfade overlap between adjacent chunks.
292pub fn concat_audio(chunks: &[&[i16]], crossfade_samples: usize) -> Vec<i16> {
293    if chunks.is_empty() {
294        return Vec::new();
295    }
296
297    if chunks.len() == 1 {
298        return chunks[0].to_vec();
299    }
300
301    if crossfade_samples == 0 {
302        // Simple concatenation
303        let total_len: usize = chunks.iter().map(|c| c.len()).sum();
304        let mut output = Vec::with_capacity(total_len);
305        for chunk in chunks {
306            output.extend_from_slice(chunk);
307        }
308        return output;
309    }
310
311    // Crossfade concatenation using overlap-add
312    let mut output = chunks[0].to_vec();
313
314    for chunk in &chunks[1..] {
315        let xfade = crossfade_samples.min(output.len()).min(chunk.len());
316
317        if xfade == 0 {
318            output.extend_from_slice(chunk);
319            continue;
320        }
321
322        // Apply crossfade to the overlap region
323        let out_start = output.len() - xfade;
324        let slope = 1.0_f64 / (xfade + 1) as f64; // pre-computed step
325        let mut t = slope;
326        for i in 0..xfade {
327            let a = output[out_start + i] as f64 * (1.0 - t);
328            let b = chunk[i] as f64 * t;
329            output[out_start + i] = (a + b).clamp(-32768.0, 32767.0) as i16;
330            t += slope; // addition instead of division per sample
331        }
332
333        // Append the rest of the chunk after the crossfade region
334        if xfade < chunk.len() {
335            output.extend_from_slice(&chunk[xfade..]);
336        }
337    }
338
339    output
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    // --- AudioFormat tests ---
347
348    #[test]
349    fn test_audio_format_mono_16bit() {
350        let fmt = AudioFormat::mono_16bit(44100);
351        assert_eq!(fmt.sample_rate, 44100);
352        assert_eq!(fmt.channels, 1);
353        assert_eq!(fmt.bits_per_sample, 16);
354    }
355
356    #[test]
357    fn test_audio_format_piper_default() {
358        let fmt = AudioFormat::piper_default();
359        assert_eq!(fmt.sample_rate, 22050);
360        assert_eq!(fmt.channels, 1);
361        assert_eq!(fmt.bits_per_sample, 16);
362    }
363
364    #[test]
365    fn test_audio_format_equality() {
366        let a = AudioFormat::mono_16bit(16000);
367        let b = AudioFormat::mono_16bit(16000);
368        let c = AudioFormat::mono_16bit(22050);
369        assert_eq!(a, b);
370        assert_ne!(a, c);
371    }
372
373    // --- resample_linear tests ---
374
375    #[test]
376    fn test_resample_linear_identity() {
377        let input: Vec<i16> = (0..100).map(|i| (i * 100) as i16).collect();
378        let output = resample_linear(&input, 22050, 22050);
379        assert_eq!(input, output);
380    }
381
382    #[test]
383    fn test_resample_linear_empty() {
384        let output = resample_linear(&[], 22050, 44100);
385        assert!(output.is_empty());
386    }
387
388    #[test]
389    fn test_resample_linear_upsample_length() {
390        let input: Vec<i16> = vec![0; 100];
391        let output = resample_linear(&input, 22050, 44100);
392        // Expect roughly 2x the number of samples
393        assert!((output.len() as f64 - 200.0).abs() < 2.0);
394    }
395
396    #[test]
397    fn test_resample_linear_downsample_length() {
398        let input: Vec<i16> = vec![0; 200];
399        let output = resample_linear(&input, 44100, 22050);
400        // Expect roughly half the number of samples
401        assert!((output.len() as f64 - 100.0).abs() < 2.0);
402    }
403
404    #[test]
405    fn test_resample_linear_preserves_dc() {
406        // A constant signal should remain constant after resampling
407        let input: Vec<i16> = vec![1000; 100];
408        let output = resample_linear(&input, 22050, 44100);
409        for &s in &output {
410            assert_eq!(s, 1000);
411        }
412    }
413
414    #[test]
415    fn test_resample_linear_zero_rate() {
416        let input: Vec<i16> = vec![100; 10];
417        assert!(resample_linear(&input, 0, 44100).is_empty());
418        assert!(resample_linear(&input, 44100, 0).is_empty());
419    }
420
421    // --- mono_to_stereo / stereo_to_mono tests ---
422
423    #[test]
424    fn test_mono_to_stereo() {
425        let mono = vec![100i16, 200, 300];
426        let stereo = mono_to_stereo(&mono);
427        assert_eq!(stereo, vec![100, 100, 200, 200, 300, 300]);
428    }
429
430    #[test]
431    fn test_stereo_to_mono() {
432        let stereo = vec![100i16, 200, 300, 400];
433        let mono = stereo_to_mono(&stereo);
434        assert_eq!(mono, vec![150, 350]);
435    }
436
437    #[test]
438    fn test_mono_stereo_roundtrip() {
439        let original = vec![100i16, -200, 300, -400, 500];
440        let stereo = mono_to_stereo(&original);
441        let back = stereo_to_mono(&stereo);
442        assert_eq!(original, back);
443    }
444
445    #[test]
446    fn test_stereo_to_mono_empty() {
447        let result = stereo_to_mono(&[]);
448        assert!(result.is_empty());
449    }
450
451    // --- i16_to_f32 / f32_to_i16 tests ---
452
453    #[test]
454    fn test_i16_to_f32_range() {
455        let samples = vec![0i16, 32767, -32768];
456        let floats = i16_to_f32(&samples);
457        assert!((floats[0]).abs() < 1e-6);
458        assert!((floats[1] - 32767.0 / 32768.0).abs() < 1e-5);
459        assert!((floats[2] - (-1.0)).abs() < 1e-5);
460    }
461
462    #[test]
463    fn test_f32_to_i16_clamping() {
464        let samples = vec![2.0f32, -2.0, 0.5];
465        let ints = f32_to_i16(&samples);
466        assert_eq!(ints[0], 32767); // clamped
467        assert_eq!(ints[1], -32768); // clamped
468        // 0.5 * 32768 = 16384
469        assert_eq!(ints[2], 16384);
470    }
471
472    #[test]
473    fn test_i16_f32_roundtrip() {
474        let original = vec![0i16, 1000, -1000, 16384, -16384];
475        let floats = i16_to_f32(&original);
476        let back = f32_to_i16(&floats);
477        // Allow +/- 1 LSB due to rounding
478        for (a, b) in original.iter().zip(back.iter()) {
479            assert!(
480                (*a as i32 - *b as i32).abs() <= 1,
481                "roundtrip mismatch: {a} vs {b}"
482            );
483        }
484    }
485
486    #[test]
487    fn test_i16_to_f32_empty() {
488        assert!(i16_to_f32(&[]).is_empty());
489    }
490
491    // --- normalize_peak tests ---
492
493    #[test]
494    fn test_normalize_peak_to_full_scale() {
495        let mut samples = vec![1000i16, -1000, 500, -500];
496        normalize_peak(&mut samples, 0.0);
497        // Peak should now be at 32767
498        let peak = samples.iter().map(|&s| (s as i32).abs()).max().unwrap();
499        assert!((peak - 32767).abs() <= 1);
500    }
501
502    #[test]
503    fn test_normalize_peak_minus_6db() {
504        let mut samples = vec![32767i16, -32767];
505        normalize_peak(&mut samples, -6.0);
506        // -6 dB ~ 0.5012, peak should be ~16422
507        let peak = samples.iter().map(|&s| (s as i32).abs()).max().unwrap();
508        let expected = (32767.0 * 10.0_f64.powf(-6.0 / 20.0)) as i32;
509        assert!(
510            (peak - expected).abs() <= 1,
511            "peak={peak}, expected={expected}"
512        );
513    }
514
515    #[test]
516    fn test_normalize_peak_silence() {
517        let mut samples = vec![0i16; 100];
518        normalize_peak(&mut samples, -1.0);
519        // Should remain silence
520        assert!(samples.iter().all(|&s| s == 0));
521    }
522
523    #[test]
524    fn test_normalize_peak_empty() {
525        let mut samples: Vec<i16> = Vec::new();
526        normalize_peak(&mut samples, -1.0);
527        assert!(samples.is_empty());
528    }
529
530    // --- rms_db tests ---
531
532    #[test]
533    fn test_rms_db_silence() {
534        let samples = vec![0i16; 100];
535        assert_eq!(rms_db(&samples), f32::NEG_INFINITY);
536    }
537
538    #[test]
539    fn test_rms_db_full_scale_square() {
540        // Full-scale square wave: RMS = 32767, dB = 20*log10(32767/32768) ~ -0.0003 dB
541        let samples = vec![32767i16; 1000];
542        let db = rms_db(&samples);
543        assert!(
544            (db - 0.0).abs() < 0.01,
545            "expected ~0 dB for full-scale, got {db}"
546        );
547    }
548
549    #[test]
550    fn test_rms_db_known_signal() {
551        // A constant signal at half amplitude: ~-6.02 dB
552        let half = (32768.0 / 2.0) as i16; // 16384
553        let samples = vec![half; 1000];
554        let db = rms_db(&samples);
555        assert!((db - (-6.02)).abs() < 0.1, "expected ~-6 dB, got {db}");
556    }
557
558    #[test]
559    fn test_rms_db_empty() {
560        assert_eq!(rms_db(&[]), f32::NEG_INFINITY);
561    }
562
563    // --- trim_silence tests ---
564
565    #[test]
566    fn test_trim_silence_basic() {
567        // Build: [0, 0, 1000, 2000, 3000, 0, 0]
568        let samples = vec![0i16, 0, 1000, 2000, 3000, 0, 0];
569        // Threshold at -30 dB ~ 32768 * 10^(-30/20) ~ 1036
570        let trimmed = trim_silence(&samples, -30.0);
571        // Should keep samples >= 1036 in absolute value: 2000 and 3000
572        assert_eq!(trimmed, &[2000, 3000]);
573    }
574
575    #[test]
576    fn test_trim_silence_all_silence() {
577        let samples = vec![0i16; 100];
578        let trimmed = trim_silence(&samples, -60.0);
579        assert!(trimmed.is_empty());
580    }
581
582    #[test]
583    fn test_trim_silence_no_silence() {
584        let samples = vec![10000i16, 20000, 30000];
585        // Very low threshold so nothing is trimmed
586        let trimmed = trim_silence(&samples, -96.0);
587        assert_eq!(trimmed, &[10000, 20000, 30000]);
588    }
589
590    #[test]
591    fn test_trim_silence_empty() {
592        let trimmed = trim_silence(&[], -30.0);
593        assert!(trimmed.is_empty());
594    }
595
596    // --- fade_in / fade_out tests ---
597
598    #[test]
599    fn test_fade_in() {
600        let mut samples = vec![10000i16; 10];
601        fade_in(&mut samples, 5);
602        // First sample should be 0 (gain = 0/5 = 0)
603        assert_eq!(samples[0], 0);
604        // Samples after fade region should be unchanged
605        assert_eq!(samples[5], 10000);
606        assert_eq!(samples[9], 10000);
607        // Fade should be monotonically increasing
608        for i in 0..4 {
609            assert!(samples[i] <= samples[i + 1]);
610        }
611    }
612
613    #[test]
614    fn test_fade_out() {
615        let mut samples = vec![10000i16; 10];
616        fade_out(&mut samples, 5);
617        // Samples before fade region should be unchanged
618        assert_eq!(samples[0], 10000);
619        assert_eq!(samples[4], 10000);
620        // Last sample: i=4, gain = 1.0 - 4.0/5.0 = 0.2, 10000 * 0.2 ≈ 1999..2000
621        assert!(
622            (samples[9] - 2000).abs() <= 1,
623            "expected ~2000, got {}",
624            samples[9]
625        );
626        // Fade should be monotonically decreasing
627        for i in 5..9 {
628            assert!(samples[i] >= samples[i + 1]);
629        }
630    }
631
632    #[test]
633    fn test_fade_in_larger_than_length() {
634        let mut samples = vec![10000i16; 3];
635        fade_in(&mut samples, 100); // fade_samples > len
636        assert_eq!(samples[0], 0);
637        // Should not panic, fade is clamped to length
638    }
639
640    #[test]
641    fn test_fade_out_larger_than_length() {
642        let mut samples = vec![10000i16; 3];
643        fade_out(&mut samples, 100); // fade_samples > len
644        // Should not panic, fade is clamped to length
645        // First sample: i=0, gain = 1.0 - 0/3 = 1.0 (unchanged)
646        assert_eq!(samples[0], 10000);
647        // Last sample: i=2, gain = 1.0 - 2/3 ≈ 0.333
648        assert!(samples[2] < samples[0], "last should be smaller than first");
649    }
650
651    // --- concat_audio tests ---
652
653    #[test]
654    fn test_concat_audio_simple() {
655        let a: Vec<i16> = vec![1, 2, 3];
656        let b: Vec<i16> = vec![4, 5, 6];
657        let result = concat_audio(&[&a, &b], 0);
658        assert_eq!(result, vec![1, 2, 3, 4, 5, 6]);
659    }
660
661    #[test]
662    fn test_concat_audio_single_chunk() {
663        let a: Vec<i16> = vec![1, 2, 3];
664        let result = concat_audio(&[&a], 0);
665        assert_eq!(result, vec![1, 2, 3]);
666    }
667
668    #[test]
669    fn test_concat_audio_empty() {
670        let result = concat_audio(&[], 0);
671        assert!(result.is_empty());
672    }
673
674    #[test]
675    fn test_concat_audio_with_crossfade_length() {
676        let a: Vec<i16> = vec![10000; 10];
677        let b: Vec<i16> = vec![10000; 10];
678        // With crossfade of 3, output should be 10+10-3 = 17 samples
679        let result = concat_audio(&[&a, &b], 3);
680        assert_eq!(result.len(), 17);
681    }
682
683    #[test]
684    fn test_concat_audio_crossfade_values() {
685        // Crossfade between constant-value chunks should stay constant
686        let a: Vec<i16> = vec![5000; 10];
687        let b: Vec<i16> = vec![5000; 10];
688        let result = concat_audio(&[&a, &b], 4);
689        // All samples should be 5000 (crossfade of equal values = same value)
690        for &s in &result {
691            assert!((s - 5000).abs() <= 1, "expected ~5000, got {s}");
692        }
693    }
694
695    #[test]
696    fn test_concat_audio_three_chunks() {
697        let a: Vec<i16> = vec![1, 2, 3];
698        let b: Vec<i16> = vec![4, 5, 6];
699        let c: Vec<i16> = vec![7, 8, 9];
700        let result = concat_audio(&[&a, &b, &c], 0);
701        assert_eq!(result, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
702    }
703
704    // --- Edge case tests ---
705
706    #[test]
707    fn test_f32_to_i16_nan() {
708        // NaN should not panic; clamping NaN is implementation-defined but
709        // Rust's f32::clamp returns NaN, which `as i16` casts to 0.
710        let result = f32_to_i16(&[f32::NAN]);
711        assert_eq!(result.len(), 1);
712        // The important thing is no panic. Rust `as` cast of NaN to i16 yields 0.
713        assert_eq!(result[0], 0);
714    }
715
716    #[test]
717    fn test_f32_to_i16_positive_infinity() {
718        let result = f32_to_i16(&[f32::INFINITY]);
719        assert_eq!(result.len(), 1);
720        assert_eq!(result[0], i16::MAX);
721    }
722
723    #[test]
724    fn test_f32_to_i16_negative_infinity() {
725        let result = f32_to_i16(&[f32::NEG_INFINITY]);
726        assert_eq!(result.len(), 1);
727        assert_eq!(result[0], i16::MIN);
728    }
729
730    #[test]
731    fn test_resample_linear_single_sample() {
732        // A single sample should not panic and should produce output
733        let input = vec![12345i16];
734        let output = resample_linear(&input, 22050, 44100);
735        assert!(!output.is_empty());
736        // The output should contain the original value (possibly repeated)
737        assert_eq!(output[0], 12345);
738    }
739
740    #[test]
741    fn test_resample_linear_zero_from_rate() {
742        let input = vec![100i16; 10];
743        let output = resample_linear(&input, 0, 44100);
744        assert!(output.is_empty());
745    }
746
747    #[test]
748    fn test_resample_linear_zero_to_rate() {
749        let input = vec![100i16; 10];
750        let output = resample_linear(&input, 44100, 0);
751        assert!(output.is_empty());
752    }
753
754    #[test]
755    fn test_fade_in_zero_fade_samples() {
756        // Zero fade_samples should be a no-op
757        let mut samples = vec![1000i16, 2000, 3000];
758        let original = samples.clone();
759        fade_in(&mut samples, 0);
760        assert_eq!(samples, original);
761    }
762
763    #[test]
764    fn test_fade_out_zero_fade_samples() {
765        // Zero fade_samples should be a no-op
766        let mut samples = vec![1000i16, 2000, 3000];
767        let original = samples.clone();
768        fade_out(&mut samples, 0);
769        assert_eq!(samples, original);
770    }
771
772    #[test]
773    fn test_stereo_to_mono_odd_length() {
774        // Odd-length input: chunks_exact(2) should drop the last sample
775        let stereo = vec![100i16, 200, 300, 400, 500];
776        let mono = stereo_to_mono(&stereo);
777        // Only two complete pairs: (100,200) and (300,400); 500 is dropped
778        assert_eq!(mono.len(), 2);
779        assert_eq!(mono[0], 150);
780        assert_eq!(mono[1], 350);
781    }
782
783    #[test]
784    fn test_normalize_peak_single_sample() {
785        let mut samples = vec![1000i16];
786        normalize_peak(&mut samples, 0.0);
787        // Single-sample peak should be scaled to 32767
788        assert!((samples[0] as i32 - 32767).abs() <= 1);
789    }
790
791    #[test]
792    fn test_trim_silence_very_low_threshold() {
793        // At -90 dB, threshold_linear ~ 32768 * 10^(-90/20) ~ 1.036,
794        // which truncates to 1 as i32, so any sample with abs >= 1 is kept.
795        let samples = vec![0i16, 1, 2, 3, 0];
796        let trimmed = trim_silence(&samples, -90.0);
797        assert_eq!(trimmed, &[1, 2, 3]);
798    }
799
800    #[test]
801    fn test_concat_audio_crossfade_exceeds_chunk_length() {
802        // crossfade_samples > chunk lengths; should clamp and not panic
803        let a: Vec<i16> = vec![5000; 3];
804        let b: Vec<i16> = vec![5000; 2];
805        let result = concat_audio(&[&a, &b], 100);
806        // crossfade is clamped to min(100, 3, 2) = 2
807        // output = 3 + 2 - 2 = 3 samples
808        assert_eq!(result.len(), 3);
809        // Since both chunks have the same constant value, all samples should
810        // be approximately 5000
811        for &s in &result {
812            assert!((s - 5000).abs() <= 1, "expected ~5000, got {s}");
813        }
814    }
815}