audiobook_forge/models/
quality.rs

1//! Audio quality profile model
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Audio quality profile with bitrate, sample rate, channels, and codec
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct QualityProfile {
9    /// Bitrate in kbps
10    pub bitrate: u32,
11    /// Sample rate in Hz
12    pub sample_rate: u32,
13    /// Number of channels (1=mono, 2=stereo)
14    pub channels: u8,
15    /// Audio codec (e.g., "mp3", "aac")
16    pub codec: String,
17    /// Duration in seconds
18    pub duration: f64,
19}
20
21impl QualityProfile {
22    /// Create a new quality profile
23    pub fn new(bitrate: u32, sample_rate: u32, channels: u8, codec: String, duration: f64) -> anyhow::Result<Self> {
24        if bitrate == 0 {
25            anyhow::bail!("Bitrate must be positive, got {}", bitrate);
26        }
27        if sample_rate == 0 {
28            anyhow::bail!("Sample rate must be positive, got {}", sample_rate);
29        }
30        if channels != 1 && channels != 2 {
31            anyhow::bail!("Channels must be 1 or 2, got {}", channels);
32        }
33
34        Ok(Self {
35            bitrate,
36            sample_rate,
37            channels,
38            codec,
39            duration,
40        })
41    }
42
43    /// Compare quality profiles to determine which is better
44    pub fn is_better_than(&self, other: &QualityProfile, prefer_stereo: bool) -> bool {
45        // Priority order:
46        // 1. Bitrate (higher is better)
47        if self.bitrate != other.bitrate {
48            return self.bitrate > other.bitrate;
49        }
50
51        // 2. Sample rate (higher is better)
52        if self.sample_rate != other.sample_rate {
53            return self.sample_rate > other.sample_rate;
54        }
55
56        // 3. Channels (stereo > mono if prefer_stereo, else mono > stereo)
57        if self.channels != other.channels {
58            if prefer_stereo {
59                return self.channels > other.channels;
60            } else {
61                return self.channels < other.channels;
62            }
63        }
64
65        // 4. Codec preference: AAC > MP3
66        let codec_priority = |codec: &str| match codec.to_lowercase().as_str() {
67            "aac" => 2,
68            "mp3" => 1,
69            _ => 0,
70        };
71
72        codec_priority(&self.codec) > codec_priority(&other.codec)
73    }
74
75    /// Check if two profiles can be concatenated without re-encoding
76    pub fn is_compatible_for_concat(&self, other: &QualityProfile) -> bool {
77        self.bitrate == other.bitrate
78            && self.sample_rate == other.sample_rate
79            && self.channels == other.channels
80            && self.codec.to_lowercase() == other.codec.to_lowercase()
81    }
82
83    /// Convert to AAC profile with equivalent or better quality
84    pub fn to_aac_equivalent(&self) -> QualityProfile {
85        // For MP3->AAC conversion, use same or higher bitrate
86        let aac_bitrate = self.bitrate.max(128); // Minimum AAC quality
87
88        QualityProfile {
89            bitrate: aac_bitrate,
90            sample_rate: self.sample_rate,
91            channels: self.channels,
92            codec: "aac".to_string(),
93            duration: self.duration,
94        }
95    }
96}
97
98impl fmt::Display for QualityProfile {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(
101            f,
102            "{}kbps, {}Hz, {}ch, {}, {:.1}s",
103            self.bitrate, self.sample_rate, self.channels, self.codec, self.duration
104        )
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_quality_creation() {
114        let profile = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
115        assert_eq!(profile.bitrate, 128);
116        assert_eq!(profile.sample_rate, 44100);
117        assert_eq!(profile.channels, 2);
118        assert_eq!(profile.codec, "aac");
119    }
120
121    #[test]
122    fn test_quality_validation() {
123        assert!(QualityProfile::new(0, 44100, 2, "aac".to_string(), 3600.0).is_err());
124        assert!(QualityProfile::new(128, 0, 2, "aac".to_string(), 3600.0).is_err());
125        assert!(QualityProfile::new(128, 44100, 3, "aac".to_string(), 3600.0).is_err());
126    }
127
128    #[test]
129    fn test_is_better_than() {
130        let high = QualityProfile::new(256, 44100, 2, "aac".to_string(), 3600.0).unwrap();
131        let low = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
132
133        assert!(high.is_better_than(&low, true));
134        assert!(!low.is_better_than(&high, true));
135    }
136
137    #[test]
138    fn test_compatibility() {
139        let profile1 = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
140        let profile2 = QualityProfile::new(128, 44100, 2, "aac".to_string(), 1800.0).unwrap();
141        let profile3 = QualityProfile::new(256, 44100, 2, "aac".to_string(), 3600.0).unwrap();
142
143        assert!(profile1.is_compatible_for_concat(&profile2));
144        assert!(!profile1.is_compatible_for_concat(&profile3));
145    }
146}