1#![allow(dead_code)]
8
9#[derive(Debug, Clone)]
11pub struct AudioTranscodeConfig {
12 pub input_codec: String,
14 pub output_codec: String,
16 pub bitrate_kbps: u32,
18 pub sample_rate: u32,
20 pub channels: u8,
22 pub normalize: bool,
24 pub target_lufs: f64,
26}
27
28impl AudioTranscodeConfig {
29 pub fn new(
31 input_codec: impl Into<String>,
32 output_codec: impl Into<String>,
33 bitrate_kbps: u32,
34 sample_rate: u32,
35 channels: u8,
36 ) -> Self {
37 Self {
38 input_codec: input_codec.into(),
39 output_codec: output_codec.into(),
40 bitrate_kbps,
41 sample_rate,
42 channels,
43 normalize: false,
44 target_lufs: -23.0,
45 }
46 }
47
48 #[must_use]
50 pub fn aac_stereo_256k() -> Self {
51 Self::new("pcm_s24le", "aac", 256, 48_000, 2)
52 }
53
54 #[must_use]
56 pub fn opus_stereo_128k() -> Self {
57 Self::new("pcm_s24le", "opus", 128, 48_000, 2)
58 }
59
60 #[must_use]
62 pub fn flac_lossless() -> Self {
63 let mut cfg = Self::new("pcm_s24le", "flac", 0, 48_000, 2);
64 cfg.bitrate_kbps = 0; cfg
66 }
67
68 #[must_use]
70 pub fn with_normalization(mut self, target_lufs: f64) -> Self {
71 self.normalize = true;
72 self.target_lufs = target_lufs;
73 self
74 }
75
76 #[must_use]
78 pub fn is_lossless_output(&self) -> bool {
79 is_lossless_codec(&self.output_codec)
80 }
81
82 #[must_use]
87 pub fn is_valid(&self) -> bool {
88 !self.output_codec.is_empty() && self.sample_rate > 0 && self.channels > 0
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct AudioTranscodeJob {
95 pub config: AudioTranscodeConfig,
97 pub input_path: String,
99 pub output_path: String,
101}
102
103impl AudioTranscodeJob {
104 pub fn new(
106 config: AudioTranscodeConfig,
107 input_path: impl Into<String>,
108 output_path: impl Into<String>,
109 ) -> Self {
110 Self {
111 config,
112 input_path: input_path.into(),
113 output_path: output_path.into(),
114 }
115 }
116
117 #[must_use]
121 pub fn estimated_output_size_bytes(&self) -> u64 {
122 if self.config.is_lossless_output() {
123 return 0;
124 }
125 0 }
127
128 #[must_use]
130 pub fn summary(&self) -> String {
131 format!(
132 "{} → {} | {} → {} | {}ch @ {}Hz | {} kbps",
133 self.input_path,
134 self.output_path,
135 self.config.input_codec,
136 self.config.output_codec,
137 self.config.channels,
138 self.config.sample_rate,
139 self.config.bitrate_kbps,
140 )
141 }
142}
143
144#[must_use]
148pub fn estimate_output_size_bytes(duration_ms: u64, bitrate_kbps: u32) -> u64 {
149 if bitrate_kbps == 0 || duration_ms == 0 {
150 return 0;
151 }
152 let bits = u64::from(bitrate_kbps) * 1000 * duration_ms / 1000;
154 bits / 8
155}
156
157#[must_use]
159pub fn channel_layout_name(channels: u8) -> &'static str {
160 match channels {
161 1 => "mono",
162 2 => "stereo",
163 3 => "2.1",
164 4 => "quad",
165 5 => "4.1",
166 6 => "5.1",
167 7 => "6.1",
168 8 => "7.1",
169 _ => "unknown",
170 }
171}
172
173#[must_use]
175pub fn is_lossless_codec(codec: &str) -> bool {
176 matches!(
177 codec.to_lowercase().as_str(),
178 "flac"
179 | "alac"
180 | "pcm_s16le"
181 | "pcm_s16be"
182 | "pcm_s24le"
183 | "pcm_s24be"
184 | "pcm_s32le"
185 | "pcm_s32be"
186 | "pcm_f32le"
187 | "pcm_f64le"
188 | "wavpack"
189 | "tta"
190 | "mlp"
191 | "truehd"
192 )
193}
194
195#[must_use]
199pub fn typical_max_bitrate_kbps(codec: &str, channels: u8) -> u32 {
200 let per_channel: u32 = match codec.to_lowercase().as_str() {
201 "opus" => 64,
202 "aac" | "aac_lc" | "he_aac" => 128,
203 "mp3" => 160,
204 "vorbis" => 96,
205 "ac3" | "eac3" => 192,
206 "dts" => 256,
207 _ => 128,
208 };
209 per_channel * u32::from(channels)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_aac_stereo_256k_config() {
218 let cfg = AudioTranscodeConfig::aac_stereo_256k();
219 assert_eq!(cfg.output_codec, "aac");
220 assert_eq!(cfg.bitrate_kbps, 256);
221 assert_eq!(cfg.sample_rate, 48_000);
222 assert_eq!(cfg.channels, 2);
223 assert!(!cfg.is_lossless_output());
224 }
225
226 #[test]
227 fn test_opus_stereo_128k_config() {
228 let cfg = AudioTranscodeConfig::opus_stereo_128k();
229 assert_eq!(cfg.output_codec, "opus");
230 assert_eq!(cfg.bitrate_kbps, 128);
231 assert_eq!(cfg.channels, 2);
232 }
233
234 #[test]
235 fn test_flac_lossless_config() {
236 let cfg = AudioTranscodeConfig::flac_lossless();
237 assert_eq!(cfg.output_codec, "flac");
238 assert_eq!(cfg.bitrate_kbps, 0);
239 assert!(cfg.is_lossless_output());
240 }
241
242 #[test]
243 fn test_config_with_normalization() {
244 let cfg = AudioTranscodeConfig::aac_stereo_256k().with_normalization(-16.0);
245 assert!(cfg.normalize);
246 assert!((cfg.target_lufs - -16.0).abs() < 1e-9);
247 }
248
249 #[test]
250 fn test_config_is_valid() {
251 let cfg = AudioTranscodeConfig::aac_stereo_256k();
252 assert!(cfg.is_valid());
253
254 let bad = AudioTranscodeConfig::new("pcm", "", 256, 48_000, 2);
255 assert!(!bad.is_valid());
256
257 let bad_rate = AudioTranscodeConfig::new("pcm", "aac", 256, 0, 2);
258 assert!(!bad_rate.is_valid());
259
260 let bad_ch = AudioTranscodeConfig::new("pcm", "aac", 256, 48_000, 0);
261 assert!(!bad_ch.is_valid());
262 }
263
264 #[test]
265 fn test_estimate_output_size_bytes() {
266 assert_eq!(estimate_output_size_bytes(10_000, 128), 160_000);
268 }
269
270 #[test]
271 fn test_estimate_output_size_bytes_zero_bitrate() {
272 assert_eq!(estimate_output_size_bytes(10_000, 0), 0);
273 }
274
275 #[test]
276 fn test_estimate_output_size_bytes_zero_duration() {
277 assert_eq!(estimate_output_size_bytes(0, 256), 0);
278 }
279
280 #[test]
281 fn test_channel_layout_name() {
282 assert_eq!(channel_layout_name(1), "mono");
283 assert_eq!(channel_layout_name(2), "stereo");
284 assert_eq!(channel_layout_name(6), "5.1");
285 assert_eq!(channel_layout_name(8), "7.1");
286 assert_eq!(channel_layout_name(10), "unknown");
287 }
288
289 #[test]
290 fn test_is_lossless_codec_known_lossless() {
291 assert!(is_lossless_codec("flac"));
292 assert!(is_lossless_codec("FLAC"));
293 assert!(is_lossless_codec("alac"));
294 assert!(is_lossless_codec("pcm_s16le"));
295 assert!(is_lossless_codec("wavpack"));
296 assert!(is_lossless_codec("truehd"));
297 }
298
299 #[test]
300 fn test_is_lossless_codec_known_lossy() {
301 assert!(!is_lossless_codec("aac"));
302 assert!(!is_lossless_codec("opus"));
303 assert!(!is_lossless_codec("mp3"));
304 assert!(!is_lossless_codec("vorbis"));
305 assert!(!is_lossless_codec("ac3"));
306 }
307
308 #[test]
309 fn test_typical_max_bitrate_stereo() {
310 let opus_stereo = typical_max_bitrate_kbps("opus", 2);
311 assert_eq!(opus_stereo, 128);
312
313 let aac_51 = typical_max_bitrate_kbps("aac", 6);
314 assert_eq!(aac_51, 768);
315 }
316
317 #[test]
318 fn test_audio_transcode_job_summary() {
319 let cfg = AudioTranscodeConfig::aac_stereo_256k();
320 let job = AudioTranscodeJob::new(cfg, "input.mxf", "output.m4a");
321 let summary = job.summary();
322 assert!(summary.contains("input.mxf"));
323 assert!(summary.contains("output.m4a"));
324 assert!(summary.contains("aac"));
325 }
326}