audiobook_forge/audio/
encoder.rs1use std::process::Command;
2use std::sync::OnceLock;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AacEncoder {
7 AppleSilicon,
9 LibFdk,
11 Native,
13}
14
15impl AacEncoder {
16 pub fn name(&self) -> &'static str {
18 match self {
19 Self::AppleSilicon => "aac_at",
20 Self::LibFdk => "libfdk_aac",
21 Self::Native => "aac",
22 }
23 }
24
25 pub fn supports_threading(&self) -> bool {
27 match self {
28 Self::AppleSilicon => false, Self::LibFdk => false, Self::Native => true, }
32 }
33
34 pub fn from_str(s: &str) -> Option<Self> {
36 match s.to_lowercase().as_str() {
37 "aac_at" => Some(Self::AppleSilicon),
38 "libfdk_aac" | "libfdk" => Some(Self::LibFdk),
39 "aac" => Some(Self::Native),
40 _ => None,
41 }
42 }
43}
44
45impl std::fmt::Display for AacEncoder {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "{}", self.name())
48 }
49}
50
51pub struct EncoderDetector;
53
54impl EncoderDetector {
55 pub fn detect_best_encoder() -> AacEncoder {
58 let candidates = [
59 AacEncoder::AppleSilicon,
60 AacEncoder::LibFdk,
61 AacEncoder::Native,
62 ];
63
64 for encoder in candidates {
65 if Self::is_encoder_available(encoder) {
66 tracing::info!("Detected AAC encoder: {}", encoder.name());
67 return encoder;
68 }
69 }
70
71 tracing::warn!("No AAC encoder detected, defaulting to 'aac'");
73 AacEncoder::Native
74 }
75
76 pub fn is_encoder_available(encoder: AacEncoder) -> bool {
78 let output = Command::new("ffmpeg").args(&["-encoders"]).output();
79
80 if let Ok(output) = output {
81 let stdout = String::from_utf8_lossy(&output.stdout);
82 stdout.lines().any(|line| {
85 let trimmed = line.trim_start();
86 trimmed.starts_with('A') && line.contains(encoder.name())
88 })
89 } else {
90 false
91 }
92 }
93
94 pub fn get_available_encoders() -> Vec<AacEncoder> {
96 let all_encoders = [
97 AacEncoder::AppleSilicon,
98 AacEncoder::LibFdk,
99 AacEncoder::Native,
100 ];
101
102 all_encoders
103 .into_iter()
104 .filter(|&encoder| Self::is_encoder_available(encoder))
105 .collect()
106 }
107}
108
109static DETECTED_ENCODER: OnceLock<AacEncoder> = OnceLock::new();
111
112pub fn get_encoder() -> AacEncoder {
114 *DETECTED_ENCODER.get_or_init(|| EncoderDetector::detect_best_encoder())
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_encoder_name() {
123 assert_eq!(AacEncoder::AppleSilicon.name(), "aac_at");
124 assert_eq!(AacEncoder::LibFdk.name(), "libfdk_aac");
125 assert_eq!(AacEncoder::Native.name(), "aac");
126 }
127
128 #[test]
129 fn test_encoder_threading() {
130 assert!(!AacEncoder::AppleSilicon.supports_threading());
131 assert!(!AacEncoder::LibFdk.supports_threading());
132 assert!(AacEncoder::Native.supports_threading());
133 }
134
135 #[test]
136 fn test_encoder_from_str() {
137 assert_eq!(AacEncoder::from_str("aac_at"), Some(AacEncoder::AppleSilicon));
138 assert_eq!(AacEncoder::from_str("libfdk_aac"), Some(AacEncoder::LibFdk));
139 assert_eq!(AacEncoder::from_str("libfdk"), Some(AacEncoder::LibFdk));
140 assert_eq!(AacEncoder::from_str("aac"), Some(AacEncoder::Native));
141 assert_eq!(AacEncoder::from_str("unknown"), None);
142 }
143
144 #[test]
145 fn test_encoder_display() {
146 assert_eq!(format!("{}", AacEncoder::AppleSilicon), "aac_at");
147 assert_eq!(format!("{}", AacEncoder::LibFdk), "libfdk_aac");
148 assert_eq!(format!("{}", AacEncoder::Native), "aac");
149 }
150
151 #[test]
152 fn test_detect_encoder() {
153 let encoder = EncoderDetector::detect_best_encoder();
155 assert!(matches!(
156 encoder,
157 AacEncoder::AppleSilicon | AacEncoder::LibFdk | AacEncoder::Native
158 ));
159 }
160
161 #[test]
162 fn test_get_available_encoders() {
163 let encoders = EncoderDetector::get_available_encoders();
164 assert!(!encoders.is_empty());
166 assert!(encoders.contains(&AacEncoder::Native) || encoders.len() > 0);
168 }
169
170 #[test]
171 fn test_get_encoder_cached() {
172 let encoder1 = get_encoder();
174 let encoder2 = get_encoder();
176 assert_eq!(encoder1, encoder2);
177 }
178}