1#![allow(dead_code)]
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum AudioLayout {
13 Mono,
15 Stereo,
17 TwoPointOne,
19 Quad,
21 FivePointZero,
23 FivePointOne,
25 SevenPointOne,
27}
28
29impl AudioLayout {
30 #[must_use]
32 pub fn channel_count(self) -> u8 {
33 match self {
34 Self::Mono => 1,
35 Self::Stereo => 2,
36 Self::TwoPointOne => 3,
37 Self::Quad => 4,
38 Self::FivePointZero => 5,
39 Self::FivePointOne => 6,
40 Self::SevenPointOne => 8,
41 }
42 }
43
44 #[must_use]
46 pub fn has_lfe(self) -> bool {
47 matches!(
48 self,
49 Self::TwoPointOne | Self::FivePointOne | Self::SevenPointOne
50 )
51 }
52
53 #[must_use]
55 pub fn label(self) -> &'static str {
56 match self {
57 Self::Mono => "mono",
58 Self::Stereo => "stereo",
59 Self::TwoPointOne => "2.1",
60 Self::Quad => "4.0",
61 Self::FivePointZero => "5.0",
62 Self::FivePointOne => "5.1",
63 Self::SevenPointOne => "7.1",
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct AudioTranscodeParams {
71 pub input_layout: AudioLayout,
73 pub output_layout: AudioLayout,
75 pub input_sample_rate: u32,
77 pub output_sample_rate: u32,
79 pub target_bitrate_bps: u32,
81 pub gain_linear: f32,
83 pub normalise_loudness: bool,
85}
86
87impl Default for AudioTranscodeParams {
88 fn default() -> Self {
89 Self {
90 input_layout: AudioLayout::Stereo,
91 output_layout: AudioLayout::Stereo,
92 input_sample_rate: 48_000,
93 output_sample_rate: 48_000,
94 target_bitrate_bps: 128_000,
95 gain_linear: 1.0,
96 normalise_loudness: false,
97 }
98 }
99}
100
101impl AudioTranscodeParams {
102 #[must_use]
104 pub fn stereo() -> Self {
105 Self::default()
106 }
107
108 #[must_use]
110 pub fn mono_downmix() -> Self {
111 Self {
112 output_layout: AudioLayout::Mono,
113 ..Self::default()
114 }
115 }
116
117 #[must_use]
120 pub fn is_passthrough(&self) -> bool {
121 self.input_layout == self.output_layout
122 && self.input_sample_rate == self.output_sample_rate
123 && (self.gain_linear - 1.0).abs() < f32::EPSILON
124 && !self.normalise_loudness
125 }
126
127 #[must_use]
129 pub fn is_valid(&self) -> bool {
130 self.input_sample_rate > 0
131 && self.output_sample_rate > 0
132 && self.target_bitrate_bps > 0
133 && self.gain_linear >= 0.0
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AudioChannelMap {
140 input_layout: AudioLayout,
141 output_layout: AudioLayout,
142 routes: Vec<(usize, f32)>,
144}
145
146impl AudioChannelMap {
147 #[must_use]
149 pub fn identity(layout: AudioLayout) -> Self {
150 let n = layout.channel_count() as usize;
151 let routes = (0..n).map(|i| (i, 1.0_f32)).collect();
152 Self {
153 input_layout: layout,
154 output_layout: layout,
155 routes,
156 }
157 }
158
159 #[must_use]
161 pub fn stereo_to_mono() -> Self {
162 Self {
163 input_layout: AudioLayout::Stereo,
164 output_layout: AudioLayout::Mono,
165 routes: vec![(0, 0.707_107), (1, 0.707_107)],
167 }
168 }
169
170 #[allow(clippy::cast_precision_loss)]
174 #[must_use]
175 pub fn compute_gain_db(&self, output_channel: usize) -> Option<f64> {
176 let (_, gain_linear) = self.routes.get(output_channel)?;
177 if *gain_linear <= 0.0 {
178 return Some(f64::NEG_INFINITY);
179 }
180 let db = 20.0 * f64::from(*gain_linear).log10();
182 Some(db)
183 }
184
185 pub fn validate_params(&self) -> Result<(), String> {
192 let expected_out = self.output_layout.channel_count() as usize;
193 if self.routes.len() < expected_out {
194 return Err(format!(
195 "route count {} is less than output channel count {}",
196 self.routes.len(),
197 expected_out
198 ));
199 }
200 let max_in = self.input_layout.channel_count() as usize;
201 for (ch_idx, _) in &self.routes {
202 if *ch_idx >= max_in {
203 return Err(format!(
204 "input channel index {ch_idx} out of range (input has {max_in} channels)"
205 ));
206 }
207 }
208 Ok(())
209 }
210
211 #[must_use]
213 pub fn output_channel_count(&self) -> usize {
214 self.output_layout.channel_count() as usize
215 }
216
217 pub fn apply(&self, input: &[f32], output: &mut [f32], frame_size: usize) {
222 let in_ch = self.input_layout.channel_count() as usize;
223 let out_ch = self.output_layout.channel_count() as usize;
224 for frame in 0..frame_size {
225 for (out_idx, (in_idx, gain)) in self.routes.iter().enumerate() {
226 let in_pos = frame * in_ch + in_idx;
227 let out_pos = frame * out_ch + out_idx;
228 if in_pos < input.len() && out_pos < output.len() {
229 output[out_pos] += input[in_pos] * gain;
230 }
231 }
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_mono_channel_count() {
242 assert_eq!(AudioLayout::Mono.channel_count(), 1);
243 }
244
245 #[test]
246 fn test_stereo_channel_count() {
247 assert_eq!(AudioLayout::Stereo.channel_count(), 2);
248 }
249
250 #[test]
251 fn test_five_one_channel_count() {
252 assert_eq!(AudioLayout::FivePointOne.channel_count(), 6);
253 }
254
255 #[test]
256 fn test_seven_one_channel_count() {
257 assert_eq!(AudioLayout::SevenPointOne.channel_count(), 8);
258 }
259
260 #[test]
261 fn test_has_lfe() {
262 assert!(AudioLayout::FivePointOne.has_lfe());
263 assert!(AudioLayout::TwoPointOne.has_lfe());
264 assert!(!AudioLayout::Stereo.has_lfe());
265 assert!(!AudioLayout::Quad.has_lfe());
266 }
267
268 #[test]
269 fn test_labels_non_empty() {
270 let layouts = [
271 AudioLayout::Mono,
272 AudioLayout::Stereo,
273 AudioLayout::TwoPointOne,
274 AudioLayout::Quad,
275 AudioLayout::FivePointZero,
276 AudioLayout::FivePointOne,
277 AudioLayout::SevenPointOne,
278 ];
279 for l in layouts {
280 assert!(!l.label().is_empty());
281 }
282 }
283
284 #[test]
285 fn test_params_is_passthrough_default() {
286 let p = AudioTranscodeParams::default();
287 assert!(p.is_passthrough());
288 }
289
290 #[test]
291 fn test_params_not_passthrough_different_layout() {
292 let p = AudioTranscodeParams {
293 output_layout: AudioLayout::Mono,
294 ..AudioTranscodeParams::default()
295 };
296 assert!(!p.is_passthrough());
297 }
298
299 #[test]
300 fn test_params_not_passthrough_with_gain() {
301 let p = AudioTranscodeParams {
302 gain_linear: 1.5,
303 ..AudioTranscodeParams::default()
304 };
305 assert!(!p.is_passthrough());
306 }
307
308 #[test]
309 fn test_params_is_valid_default() {
310 assert!(AudioTranscodeParams::default().is_valid());
311 }
312
313 #[test]
314 fn test_params_invalid_zero_sample_rate() {
315 let p = AudioTranscodeParams {
316 input_sample_rate: 0,
317 ..AudioTranscodeParams::default()
318 };
319 assert!(!p.is_valid());
320 }
321
322 #[test]
323 fn test_identity_map_validate() {
324 let m = AudioChannelMap::identity(AudioLayout::Stereo);
325 assert!(m.validate_params().is_ok());
326 }
327
328 #[test]
329 fn test_stereo_to_mono_validate() {
330 let m = AudioChannelMap::stereo_to_mono();
331 assert!(m.validate_params().is_ok());
332 }
333
334 #[test]
335 fn test_compute_gain_db_unity() {
336 let m = AudioChannelMap::identity(AudioLayout::Stereo);
337 let db = m.compute_gain_db(0).expect("should succeed in test");
338 assert!(
339 (db - 0.0).abs() < 1e-9,
340 "unity gain should be 0 dB, got {db}"
341 );
342 }
343
344 #[test]
345 fn test_compute_gain_db_stereo_to_mono() {
346 let m = AudioChannelMap::stereo_to_mono();
347 let db = m.compute_gain_db(0).expect("should succeed in test");
348 assert!((db + 3.01).abs() < 0.1, "expected ~-3 dB, got {db}");
350 }
351
352 #[test]
353 fn test_compute_gain_db_out_of_range() {
354 let m = AudioChannelMap::identity(AudioLayout::Mono);
355 assert!(m.compute_gain_db(5).is_none());
356 }
357
358 #[test]
359 fn test_apply_identity() {
360 let m = AudioChannelMap::identity(AudioLayout::Stereo);
361 let input = vec![0.5_f32, 0.8, 0.3, 0.1];
362 let mut output = vec![0.0_f32; 4];
363 m.apply(&input, &mut output, 2);
364 assert!((output[0] - 0.5).abs() < 1e-6);
365 assert!((output[1] - 0.8).abs() < 1e-6);
366 }
367
368 #[test]
369 fn test_output_channel_count() {
370 let m = AudioChannelMap::stereo_to_mono();
371 assert_eq!(m.output_channel_count(), 1);
372 }
373}