speech_prep/decoder/
mixer.rs1use crate::error::{Error, Result};
2
3use super::MixedAudio;
4
5#[derive(Debug, Default, Clone, Copy)]
7pub struct ChannelMixer;
8
9impl ChannelMixer {
10 #[must_use]
12 pub const fn new() -> Self {
13 Self
14 }
15
16 pub fn mix_to_mono(samples: &[f32], channels: u8) -> Result<MixedAudio> {
18 if channels == 0 {
19 return Err(Error::InvalidInput("channel count cannot be zero".into()));
20 }
21 if ![1, 2, 4, 6].contains(&channels) {
22 return Err(Error::InvalidInput(format!(
23 "unsupported channel count: {channels} (supports 1, 2, 4, 6 only)"
24 )));
25 }
26 if !samples.len().is_multiple_of(usize::from(channels)) {
27 let sample_len = samples.len();
28 return Err(Error::InvalidInput(format!(
29 "sample count {sample_len} not divisible by channel count {channels}"
30 )));
31 }
32
33 let peak_before = Self::calculate_peak(samples);
34
35 if channels == 1 {
36 return Ok(MixedAudio {
37 samples: samples.to_vec(),
38 original_channels: 1,
39 peak_before_mix: peak_before,
40 peak_after_mix: peak_before,
41 });
42 }
43
44 let frame_count = samples.len() / usize::from(channels);
45 let mut mixed = Vec::with_capacity(frame_count);
46
47 for frame in samples.chunks_exact(usize::from(channels)) {
48 let sum: f32 = frame.iter().sum();
49 let avg = sum / f32::from(channels);
50 mixed.push(avg.clamp(-1.0, 1.0));
51 }
52
53 let peak_after = Self::calculate_peak(&mixed);
54
55 Ok(MixedAudio {
56 samples: mixed,
57 original_channels: channels,
58 peak_before_mix: peak_before,
59 peak_after_mix: peak_after,
60 })
61 }
62
63 #[inline]
64 fn calculate_peak(samples: &[f32]) -> f32 {
65 samples.iter().map(|s| s.abs()).fold(0.0f32, f32::max)
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 type TestResult<T> = std::result::Result<T, String>;
74
75 #[test]
76 fn test_mix_identity_for_mono() -> TestResult<()> {
77 let mono = vec![0.1, -0.2, 0.3];
78 let mixed = ChannelMixer::mix_to_mono(&mono, 1).map_err(|e| e.to_string())?;
79 assert_eq!(mixed.samples, mono);
80 Ok(())
81 }
82
83 #[test]
84 fn test_mix_stereo_to_mono() -> TestResult<()> {
85 let stereo = vec![0.5, -0.5, 0.8, 0.2];
86 let mixed = ChannelMixer::mix_to_mono(&stereo, 2).map_err(|e| e.to_string())?;
87 assert_eq!(mixed.samples.len(), 2);
88 Ok(())
89 }
90
91 #[test]
92 fn test_mix_reject_invalid_channels() {
93 let samples = vec![0.0, 0.0, 0.0];
94 assert!(ChannelMixer::mix_to_mono(&samples, 3).is_err());
95 }
96
97 #[test]
98 fn test_mix_reject_misaligned_samples() {
99 let samples = vec![0.0, 0.0, 0.0];
100 assert!(ChannelMixer::mix_to_mono(&samples, 2).is_err());
101 }
102
103 #[test]
104 fn test_mix_empty_input() -> TestResult<()> {
105 let empty: Vec<f32> = Vec::new();
106 let mixed = ChannelMixer::mix_to_mono(&empty, 1).map_err(|e| e.to_string())?;
107 assert_eq!(mixed.samples.len(), 0);
108 assert_eq!(mixed.sample_count(), 0);
109 Ok(())
110 }
111
112 #[test]
113 fn test_mix_single_frame_stereo() -> TestResult<()> {
114 let stereo = vec![0.6, 0.4];
115 let mixed = ChannelMixer::mix_to_mono(&stereo, 2).map_err(|e| e.to_string())?;
116 assert_eq!(mixed.samples.len(), 1);
117 assert!((mixed.samples[0] - 0.5).abs() < f32::EPSILON);
118 Ok(())
119 }
120
121 #[test]
122 fn test_mix_is_clipped_detection() -> TestResult<()> {
123 let clipped = vec![1.0, 1.0];
124 let unclipped = vec![0.5, 0.5];
125
126 let mixed_clipped = ChannelMixer::mix_to_mono(&clipped, 2).map_err(|e| e.to_string())?;
127 assert!(mixed_clipped.is_clipped());
128
129 let mixed_unclipped =
130 ChannelMixer::mix_to_mono(&unclipped, 2).map_err(|e| e.to_string())?;
131 assert!(!mixed_unclipped.is_clipped());
132 Ok(())
133 }
134
135 #[test]
136 fn test_mix_peak_ratio_behavior() -> TestResult<()> {
137 let stereo = vec![0.8, -0.8, 0.4, -0.4];
138 let mixed = ChannelMixer::mix_to_mono(&stereo, 2).map_err(|e| e.to_string())?;
139 assert!(mixed.peak_before_mix > 0.0);
140 assert!(mixed.peak_ratio() <= 1.0);
141 Ok(())
142 }
143}