1use crate::{Result, TranscodeError};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum AudioCodecId {
11 Opus,
13 Flac,
15 Vorbis,
17 Mp3,
19 Aac,
21 Pcm,
23}
24
25impl AudioCodecId {
26 #[must_use]
28 pub fn name(self) -> &'static str {
29 match self {
30 Self::Opus => "opus",
31 Self::Flac => "flac",
32 Self::Vorbis => "vorbis",
33 Self::Mp3 => "mp3",
34 Self::Aac => "aac",
35 Self::Pcm => "pcm",
36 }
37 }
38
39 #[must_use]
41 pub fn is_lossless(self) -> bool {
42 matches!(self, Self::Flac | Self::Pcm)
43 }
44
45 #[must_use]
47 pub fn default_bitrate(self) -> u32 {
48 match self {
49 Self::Opus => 128_000,
50 Self::Flac => 0, Self::Vorbis => 128_000,
52 Self::Mp3 => 192_000,
53 Self::Aac => 192_000,
54 Self::Pcm => 0, }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct AudioOnlyConfig {
62 pub input_codec: AudioCodecId,
64 pub output_codec: AudioCodecId,
66 pub sample_rate: u32,
68 pub channels: u8,
70 pub bitrate: u32,
73}
74
75impl AudioOnlyConfig {
76 pub fn new(
84 input_codec: AudioCodecId,
85 output_codec: AudioCodecId,
86 sample_rate: u32,
87 channels: u8,
88 bitrate: u32,
89 ) -> Result<Self> {
90 if channels == 0 || channels > 8 {
91 return Err(TranscodeError::InvalidInput(format!(
92 "channels must be 1–8, got {channels}"
93 )));
94 }
95 if sample_rate < 8_000 || sample_rate > 192_000 {
96 return Err(TranscodeError::InvalidInput(format!(
97 "sample_rate must be 8000–192000 Hz, got {sample_rate}"
98 )));
99 }
100 Ok(Self {
101 input_codec,
102 output_codec,
103 sample_rate,
104 channels,
105 bitrate,
106 })
107 }
108
109 #[must_use]
115 pub fn opus_stereo() -> Self {
116 Self::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 128_000)
117 .expect("hard-coded opus_stereo config is always valid")
118 }
119
120 #[must_use]
126 pub fn flac_stereo() -> Self {
127 Self::new(AudioCodecId::Pcm, AudioCodecId::Flac, 48_000, 2, 0)
128 .expect("hard-coded flac_stereo config is always valid")
129 }
130}
131
132pub struct AudioOnlyTranscoder {
141 config: AudioOnlyConfig,
142 frames_processed: u64,
144}
145
146impl AudioOnlyTranscoder {
147 #[must_use]
149 pub fn new(config: AudioOnlyConfig) -> Self {
150 Self {
151 config,
152 frames_processed: 0,
153 }
154 }
155
156 #[must_use]
158 pub fn config(&self) -> &AudioOnlyConfig {
159 &self.config
160 }
161
162 #[must_use]
164 pub fn codec_name(&self) -> &str {
165 self.config.output_codec.name()
166 }
167
168 #[must_use]
172 pub fn estimated_bitrate(&self) -> u32 {
173 if self.config.bitrate == 0 {
174 self.config.output_codec.default_bitrate()
175 } else {
176 self.config.bitrate
177 }
178 }
179
180 #[must_use]
182 pub fn frames_processed(&self) -> u64 {
183 self.frames_processed
184 }
185
186 pub fn transcode_samples(&mut self, input: &[f32]) -> Result<Vec<f32>> {
199 let ch = self.config.channels as usize;
200 if ch == 0 {
201 return Err(TranscodeError::InvalidInput(
202 "channel count must not be zero".to_string(),
203 ));
204 }
205 if input.len() % ch != 0 {
206 return Err(TranscodeError::InvalidInput(format!(
207 "input length {} is not a multiple of channel count {}",
208 input.len(),
209 ch
210 )));
211 }
212 for (idx, &s) in input.iter().enumerate() {
214 if s.is_nan() || s.is_infinite() {
215 return Err(TranscodeError::InvalidInput(format!(
216 "sample at index {idx} is non-finite: {s}"
217 )));
218 }
219 }
220
221 let num_frames = input.len() / ch;
222 self.frames_processed += num_frames as u64;
223
224 let gain = self.codec_gain_factor();
227 let output: Vec<f32> = input.iter().map(|&s| s * gain).collect();
228
229 Ok(output)
230 }
231
232 pub fn reset(&mut self) {
234 self.frames_processed = 0;
235 }
236
237 pub fn update_config(
243 &mut self,
244 input_codec: AudioCodecId,
245 output_codec: AudioCodecId,
246 sample_rate: u32,
247 channels: u8,
248 bitrate: u32,
249 ) -> Result<()> {
250 let new_cfg =
251 AudioOnlyConfig::new(input_codec, output_codec, sample_rate, channels, bitrate)?;
252 self.config = new_cfg;
253 Ok(())
254 }
255
256 fn codec_gain_factor(&self) -> f32 {
263 match self.config.output_codec {
264 AudioCodecId::Flac | AudioCodecId::Pcm => 1.0, AudioCodecId::Opus => 0.9999,
266 AudioCodecId::Vorbis => 0.9998,
267 AudioCodecId::Mp3 => 0.9997,
268 AudioCodecId::Aac => 0.9996,
269 }
270 }
271}
272
273#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
286 fn test_codec_id_names() {
287 assert_eq!(AudioCodecId::Opus.name(), "opus");
288 assert_eq!(AudioCodecId::Flac.name(), "flac");
289 assert_eq!(AudioCodecId::Vorbis.name(), "vorbis");
290 assert_eq!(AudioCodecId::Mp3.name(), "mp3");
291 assert_eq!(AudioCodecId::Aac.name(), "aac");
292 assert_eq!(AudioCodecId::Pcm.name(), "pcm");
293 }
294
295 #[test]
296 fn test_codec_lossless_flag() {
297 assert!(AudioCodecId::Flac.is_lossless());
298 assert!(AudioCodecId::Pcm.is_lossless());
299 assert!(!AudioCodecId::Opus.is_lossless());
300 assert!(!AudioCodecId::Vorbis.is_lossless());
301 assert!(!AudioCodecId::Mp3.is_lossless());
302 assert!(!AudioCodecId::Aac.is_lossless());
303 }
304
305 #[test]
306 fn test_codec_default_bitrate() {
307 assert_eq!(AudioCodecId::Flac.default_bitrate(), 0);
308 assert_eq!(AudioCodecId::Pcm.default_bitrate(), 0);
309 assert!(AudioCodecId::Opus.default_bitrate() > 0);
310 assert!(AudioCodecId::Mp3.default_bitrate() > 0);
311 }
312
313 #[test]
318 fn test_config_valid_creation() {
319 let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 128_000);
320 assert!(cfg.is_ok(), "Valid config should succeed");
321 let cfg = cfg.expect("already checked");
322 assert_eq!(cfg.sample_rate, 48_000);
323 assert_eq!(cfg.channels, 2);
324 assert_eq!(cfg.bitrate, 128_000);
325 }
326
327 #[test]
328 fn test_config_invalid_channels_zero() {
329 let result =
330 AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 0, 128_000);
331 assert!(result.is_err(), "channels=0 must fail");
332 let msg = result.expect_err("expected error").to_string();
333 assert!(
334 msg.contains("channels"),
335 "Error should mention 'channels': {msg}"
336 );
337 }
338
339 #[test]
340 fn test_config_invalid_channels_too_many() {
341 let result =
342 AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 9, 128_000);
343 assert!(result.is_err(), "channels=9 must fail");
344 }
345
346 #[test]
347 fn test_config_invalid_sample_rate_too_low() {
348 let result = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 7_999, 2, 128_000);
349 assert!(result.is_err(), "sample_rate=7999 must fail");
350 let msg = result.expect_err("expected error").to_string();
351 assert!(
352 msg.contains("sample_rate"),
353 "Error should mention 'sample_rate': {msg}"
354 );
355 }
356
357 #[test]
358 fn test_config_invalid_sample_rate_too_high() {
359 let result =
360 AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 192_001, 2, 128_000);
361 assert!(result.is_err(), "sample_rate=192001 must fail");
362 }
363
364 #[test]
365 fn test_config_boundary_sample_rates() {
366 let low = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Pcm, 8_000, 1, 0);
368 assert!(low.is_ok(), "sample_rate=8000 should be valid");
369
370 let high = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Pcm, 192_000, 1, 0);
372 assert!(high.is_ok(), "sample_rate=192000 should be valid");
373 }
374
375 #[test]
376 fn test_config_shortcuts() {
377 let opus = AudioOnlyConfig::opus_stereo();
378 assert_eq!(opus.output_codec, AudioCodecId::Opus);
379 assert_eq!(opus.channels, 2);
380 assert_eq!(opus.sample_rate, 48_000);
381
382 let flac = AudioOnlyConfig::flac_stereo();
383 assert_eq!(flac.output_codec, AudioCodecId::Flac);
384 assert_eq!(flac.channels, 2);
385 assert!(flac.output_codec.is_lossless());
386 }
387
388 #[test]
393 fn test_transcoder_creation() {
394 let cfg = AudioOnlyConfig::opus_stereo();
395 let t = AudioOnlyTranscoder::new(cfg);
396 assert_eq!(t.codec_name(), "opus");
397 assert_eq!(t.frames_processed(), 0);
398 }
399
400 #[test]
401 fn test_transcoder_estimated_bitrate_from_config() {
402 let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 256_000)
403 .expect("valid");
404 let t = AudioOnlyTranscoder::new(cfg);
405 assert_eq!(t.estimated_bitrate(), 256_000);
406 }
407
408 #[test]
409 fn test_transcoder_estimated_bitrate_uses_default_when_zero() {
410 let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 2, 0)
411 .expect("valid");
412 let t = AudioOnlyTranscoder::new(cfg);
413 assert_eq!(t.estimated_bitrate(), AudioCodecId::Opus.default_bitrate());
414 }
415
416 #[test]
417 fn test_transcode_samples_sine_wave() {
418 let cfg = AudioOnlyConfig::opus_stereo();
419 let mut t = AudioOnlyTranscoder::new(cfg);
420
421 let sample_rate = 48_000.0f32;
423 let freq = 1_000.0f32;
424 let num_frames = 100_usize;
425 let mut input = Vec::with_capacity(num_frames * 2);
426 for i in 0..num_frames {
427 let s = 0.5 * (2.0 * std::f32::consts::PI * freq * i as f32 / sample_rate).sin();
428 input.push(s); input.push(s); }
431
432 let result = t.transcode_samples(&input);
433 assert!(
434 result.is_ok(),
435 "transcode_samples must succeed: {:?}",
436 result.err()
437 );
438 let output = result.expect("already checked");
439 assert_eq!(output.len(), input.len(), "Output length must match input");
440 assert_eq!(t.frames_processed(), num_frames as u64);
441 }
442
443 #[test]
444 fn test_transcode_samples_rejects_nan() {
445 let cfg = AudioOnlyConfig::opus_stereo();
446 let mut t = AudioOnlyTranscoder::new(cfg);
447 let input = vec![0.1f32, f32::NAN, 0.3, 0.4];
448 let result = t.transcode_samples(&input);
449 assert!(result.is_err(), "NaN sample must be rejected");
450 }
451
452 #[test]
453 fn test_transcode_samples_rejects_infinite() {
454 let cfg = AudioOnlyConfig::opus_stereo();
455 let mut t = AudioOnlyTranscoder::new(cfg);
456 let input = vec![0.1f32, f32::INFINITY, 0.3, 0.4];
457 let result = t.transcode_samples(&input);
458 assert!(result.is_err(), "Infinite sample must be rejected");
459 }
460
461 #[test]
462 fn test_transcode_samples_rejects_misaligned_input() {
463 let cfg = AudioOnlyConfig::opus_stereo(); let mut t = AudioOnlyTranscoder::new(cfg);
465 let input = vec![0.1f32, 0.2, 0.3];
467 let result = t.transcode_samples(&input);
468 assert!(result.is_err(), "Misaligned input must be rejected");
469 }
470
471 #[test]
472 fn test_transcode_samples_empty_input_succeeds() {
473 let cfg = AudioOnlyConfig::opus_stereo();
474 let mut t = AudioOnlyTranscoder::new(cfg);
475 let result = t.transcode_samples(&[]);
476 assert!(result.is_ok());
477 assert_eq!(result.expect("ok").len(), 0);
478 }
479
480 #[test]
481 fn test_reset_clears_frame_count() {
482 let cfg = AudioOnlyConfig::opus_stereo();
483 let mut t = AudioOnlyTranscoder::new(cfg);
484 let input = vec![0.1f32, 0.2]; t.transcode_samples(&input).expect("ok");
486 assert_eq!(t.frames_processed(), 1);
487 t.reset();
488 assert_eq!(t.frames_processed(), 0);
489 }
490
491 #[test]
492 fn test_update_config_valid() {
493 let cfg = AudioOnlyConfig::opus_stereo();
494 let mut t = AudioOnlyTranscoder::new(cfg);
495 let result = t.update_config(AudioCodecId::Pcm, AudioCodecId::Flac, 44_100, 2, 0);
496 assert!(
497 result.is_ok(),
498 "update_config should succeed: {:?}",
499 result.err()
500 );
501 assert_eq!(t.codec_name(), "flac");
502 }
503
504 #[test]
505 fn test_update_config_invalid_channels() {
506 let cfg = AudioOnlyConfig::opus_stereo();
507 let mut t = AudioOnlyTranscoder::new(cfg);
508 let result = t.update_config(AudioCodecId::Pcm, AudioCodecId::Opus, 48_000, 0, 128_000);
509 assert!(result.is_err(), "Invalid channels in update should fail");
510 }
511
512 #[test]
513 fn test_lossless_output_gain_is_unity() {
514 let cfg = AudioOnlyConfig::new(AudioCodecId::Pcm, AudioCodecId::Flac, 48_000, 2, 0)
515 .expect("valid");
516 let mut t = AudioOnlyTranscoder::new(cfg);
517 let input = vec![0.5f32, 0.5f32]; let output = t.transcode_samples(&input).expect("ok");
519 assert!(
521 (output[0] - input[0]).abs() < f32::EPSILON,
522 "FLAC should be lossless (gain=1.0)"
523 );
524 }
525}