audiobook_forge/audio/
encoder.rs

1use std::process::Command;
2use std::sync::OnceLock;
3
4/// AAC encoder types supported by audiobook-forge
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AacEncoder {
7    /// Apple Silicon hardware encoder (aac_at) - macOS only
8    AppleSilicon,
9    /// Fraunhofer FDK AAC encoder (libfdk_aac) - high quality
10    LibFdk,
11    /// FFmpeg native AAC encoder (aac) - universal fallback
12    Native,
13}
14
15impl AacEncoder {
16    /// Returns the FFmpeg encoder name
17    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    /// Returns whether this encoder benefits from multi-threading
26    pub fn supports_threading(&self) -> bool {
27        match self {
28            Self::AppleSilicon => false, // Hardware encoder, no threading needed
29            Self::LibFdk => false,       // Single-threaded by design
30            Self::Native => true,        // Benefits from multi-threading
31        }
32    }
33
34    /// Try to parse encoder from string
35    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
51/// Encoder detection and selection
52pub struct EncoderDetector;
53
54impl EncoderDetector {
55    /// Detect the best available AAC encoder
56    /// Priority: aac_at → libfdk_aac → aac
57    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        // Fallback (should never happen as 'aac' is always available)
72        tracing::warn!("No AAC encoder detected, defaulting to 'aac'");
73        AacEncoder::Native
74    }
75
76    /// Check if a specific encoder is available in FFmpeg
77    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            // Look for the encoder name in the output
83            // Format: " A..... encodername     Description"
84            stdout.lines().any(|line| {
85                let trimmed = line.trim_start();
86                // Check if it's an audio encoder line and contains our encoder name
87                trimmed.starts_with('A') && line.contains(encoder.name())
88            })
89        } else {
90            false
91        }
92    }
93
94    /// Get all available AAC encoders
95    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
109/// Global cache for detected encoder
110static DETECTED_ENCODER: OnceLock<AacEncoder> = OnceLock::new();
111
112/// Get the best available AAC encoder (cached, thread-safe)
113pub 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        // Should detect at least the native 'aac' encoder
154        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        // Should have at least one encoder (aac is universal)
165        assert!(!encoders.is_empty());
166        // Native aac should always be available
167        assert!(encoders.contains(&AacEncoder::Native) || encoders.len() > 0);
168    }
169
170    #[test]
171    fn test_get_encoder_cached() {
172        // First call initializes
173        let encoder1 = get_encoder();
174        // Second call should return same cached value
175        let encoder2 = get_encoder();
176        assert_eq!(encoder1, encoder2);
177    }
178}