1use std::path::PathBuf;
7use std::time::Instant;
8
9use ff_format::AudioFrame;
10
11use super::codec_options::{AudioCodecOptions, Mp3Quality};
12use super::encoder_inner::{AudioEncoderConfig, AudioEncoderInner};
13use crate::{AudioCodec, EncodeError, OutputContainer};
14
15pub struct AudioEncoderBuilder {
31 pub(crate) path: PathBuf,
32 pub(crate) container: Option<OutputContainer>,
33 pub(crate) audio_sample_rate: Option<u32>,
34 pub(crate) audio_channels: Option<u32>,
35 pub(crate) audio_codec: AudioCodec,
36 pub(crate) audio_bitrate: Option<u64>,
37 pub(crate) codec_options: Option<AudioCodecOptions>,
38 pub(crate) codec_opts: Vec<(String, String)>,
40 pub(crate) audio_codec_explicit: bool,
41}
42
43impl AudioEncoderBuilder {
44 pub(crate) fn new(path: PathBuf) -> Self {
45 Self {
46 path,
47 container: None,
48 audio_sample_rate: None,
49 audio_channels: None,
50 audio_codec: AudioCodec::default(),
51 audio_bitrate: None,
52 codec_options: None,
53 codec_opts: Vec::new(),
54 audio_codec_explicit: false,
55 }
56 }
57
58 #[must_use]
60 pub fn audio(mut self, sample_rate: u32, channels: u32) -> Self {
61 self.audio_sample_rate = Some(sample_rate);
62 self.audio_channels = Some(channels);
63 self
64 }
65
66 #[must_use]
68 pub fn audio_codec(mut self, codec: AudioCodec) -> Self {
69 self.audio_codec = codec;
70 self.audio_codec_explicit = true;
71 self
72 }
73
74 #[must_use]
76 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
77 self.audio_bitrate = Some(bitrate);
78 self
79 }
80
81 #[must_use]
83 pub fn container(mut self, container: OutputContainer) -> Self {
84 self.container = Some(container);
85 self
86 }
87
88 #[must_use]
107 pub fn codec_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
108 self.codec_opts.push((key.into(), value.into()));
109 self
110 }
111
112 #[must_use]
117 pub fn codec_options(mut self, opts: AudioCodecOptions) -> Self {
118 self.codec_options = Some(opts);
119 self
120 }
121
122 fn apply_container_defaults(&mut self) {
123 let is_flac = self
124 .path
125 .extension()
126 .and_then(|e| e.to_str())
127 .is_some_and(|e| e.eq_ignore_ascii_case("flac"))
128 || self
129 .container
130 .as_ref()
131 .is_some_and(|c| *c == OutputContainer::Flac);
132 if is_flac && !self.audio_codec_explicit {
133 self.audio_codec = AudioCodec::Flac;
134 }
135
136 let is_ogg = self
137 .path
138 .extension()
139 .and_then(|e| e.to_str())
140 .is_some_and(|e| e.eq_ignore_ascii_case("ogg"))
141 || self
142 .container
143 .as_ref()
144 .is_some_and(|c| *c == OutputContainer::Ogg);
145 if is_ogg && !self.audio_codec_explicit {
146 self.audio_codec = AudioCodec::Vorbis;
147 }
148 }
149
150 pub fn build(self) -> Result<AudioEncoder, EncodeError> {
157 AudioEncoder::from_builder(self)
158 }
159}
160
161pub struct AudioEncoder {
177 inner: Option<AudioEncoderInner>,
178 _config: AudioEncoderConfig,
179 _start_time: Instant,
180}
181
182impl AudioEncoder {
183 pub fn create<P: AsRef<std::path::Path>>(path: P) -> AudioEncoderBuilder {
188 AudioEncoderBuilder::new(path.as_ref().to_path_buf())
189 }
190
191 pub(crate) fn from_builder(mut builder: AudioEncoderBuilder) -> Result<Self, EncodeError> {
192 builder.apply_container_defaults();
193
194 let is_flac = builder
196 .path
197 .extension()
198 .and_then(|e| e.to_str())
199 .is_some_and(|e| e.eq_ignore_ascii_case("flac"))
200 || builder
201 .container
202 .as_ref()
203 .is_some_and(|c| *c == OutputContainer::Flac);
204 if is_flac && !matches!(builder.audio_codec, AudioCodec::Flac) {
205 return Err(EncodeError::UnsupportedContainerCodecCombination {
206 container: "flac".to_string(),
207 codec: builder.audio_codec.name().to_string(),
208 hint: "FLAC container only supports the FLAC codec".to_string(),
209 });
210 }
211
212 let is_ogg = builder
214 .path
215 .extension()
216 .and_then(|e| e.to_str())
217 .is_some_and(|e| e.eq_ignore_ascii_case("ogg"))
218 || builder
219 .container
220 .as_ref()
221 .is_some_and(|c| *c == OutputContainer::Ogg);
222 if is_ogg && !matches!(builder.audio_codec, AudioCodec::Vorbis | AudioCodec::Opus) {
223 return Err(EncodeError::UnsupportedContainerCodecCombination {
224 container: "ogg".to_string(),
225 codec: builder.audio_codec.name().to_string(),
226 hint: "OGG container supports Vorbis and Opus".to_string(),
227 });
228 }
229
230 if let Some(AudioCodecOptions::Opus(ref opts)) = builder.codec_options
232 && let Some(dur) = opts.frame_duration_ms
233 && ![2u32, 5, 10, 20, 40, 60].contains(&dur)
234 {
235 return Err(EncodeError::InvalidOption {
236 name: "frame_duration_ms".to_string(),
237 reason: "must be one of: 2, 5, 10, 20, 40, 60".to_string(),
238 });
239 }
240 if let Some(AudioCodecOptions::Aac(ref opts)) = builder.codec_options
241 && let Some(q) = opts.vbr_quality
242 && !(1..=5).contains(&q)
243 {
244 return Err(EncodeError::InvalidOption {
245 name: "vbr_quality".to_string(),
246 reason: "must be 1–5".to_string(),
247 });
248 }
249 if let Some(AudioCodecOptions::Mp3(ref opts)) = builder.codec_options
250 && let Mp3Quality::Vbr(q) = opts.quality
251 && q > 9
252 {
253 return Err(EncodeError::InvalidOption {
254 name: "vbr_quality".to_string(),
255 reason: "must be 0–9 (0=best)".to_string(),
256 });
257 }
258 if let Some(AudioCodecOptions::Flac(ref opts)) = builder.codec_options
259 && opts.compression_level > 12
260 {
261 return Err(EncodeError::InvalidOption {
262 name: "compression_level".to_string(),
263 reason: "must be 0–12".to_string(),
264 });
265 }
266
267 if let Some(ch) = builder.audio_channels
269 && ch > 8
270 {
271 log::warn!("audio channel count out of range count={ch} maximum=8");
272 return Err(EncodeError::InvalidChannelCount { count: ch });
273 }
274 if let Some(sr) = builder.audio_sample_rate
275 && !(8_000..=384_000).contains(&sr)
276 {
277 log::warn!("audio sample rate out of range rate={sr} minimum=8000 maximum=384000");
278 return Err(EncodeError::InvalidSampleRate { rate: sr });
279 }
280
281 let config = AudioEncoderConfig {
282 path: builder.path.clone(),
283 sample_rate: builder
284 .audio_sample_rate
285 .ok_or_else(|| EncodeError::InvalidConfig {
286 reason: "Audio sample rate not configured".to_string(),
287 })?,
288 channels: builder
289 .audio_channels
290 .ok_or_else(|| EncodeError::InvalidConfig {
291 reason: "Audio channels not configured".to_string(),
292 })?,
293 codec: builder.audio_codec,
294 bitrate: builder.audio_bitrate,
295 codec_options: builder.codec_options,
296 codec_opts: builder.codec_opts,
297 _progress_callback: false,
298 };
299
300 let inner = Some(AudioEncoderInner::new(&config)?);
301
302 Ok(Self {
303 inner,
304 _config: config,
305 _start_time: Instant::now(),
306 })
307 }
308
309 #[must_use]
311 pub fn actual_codec(&self) -> &str {
312 self.inner
313 .as_ref()
314 .map_or("", |inner| inner.actual_codec.as_str())
315 }
316
317 pub fn push(&mut self, frame: &AudioFrame) -> Result<(), EncodeError> {
323 let inner = self
324 .inner
325 .as_mut()
326 .ok_or_else(|| EncodeError::InvalidConfig {
327 reason: "Audio encoder not initialized".to_string(),
328 })?;
329 inner.push_frame(frame)?;
330 Ok(())
331 }
332
333 pub fn finish(mut self) -> Result<(), EncodeError> {
339 if let Some(mut inner) = self.inner.take() {
340 inner.finish()?;
341 }
342 Ok(())
343 }
344}
345
346impl Drop for AudioEncoder {
347 fn drop(&mut self) {
348 }
350}
351
352#[cfg(test)]
353mod tests {
354 #[test]
355 fn audio_codec_opt_should_collect_pairs_in_order() {
356 let builder = crate::AudioEncoder::create("out.m4a")
357 .codec_opt("frame_duration", "20")
358 .codec_opt("vbr", "on");
359 assert_eq!(
360 builder.codec_opts,
361 vec![
362 ("frame_duration".to_string(), "20".to_string()),
363 ("vbr".to_string(), "on".to_string()),
364 ],
365 "pairs must be kept in call order"
366 );
367 }
368
369 use super::*;
370
371 #[test]
372 fn create_should_return_builder_without_error() {
373 let _builder: AudioEncoderBuilder = AudioEncoder::create("output.m4a");
374 }
375
376 #[test]
377 fn builder_audio_settings_should_be_stored() {
378 let builder = AudioEncoder::create("output.m4a")
379 .audio(48000, 2)
380 .audio_codec(AudioCodec::Aac)
381 .audio_bitrate(192_000);
382 assert_eq!(builder.audio_sample_rate, Some(48000));
383 assert_eq!(builder.audio_channels, Some(2));
384 assert_eq!(builder.audio_codec, AudioCodec::Aac);
385 assert_eq!(builder.audio_bitrate, Some(192_000));
386 }
387
388 #[test]
389 fn build_without_sample_rate_should_return_error() {
390 let result = AudioEncoder::create("output.m4a").build();
391 assert!(result.is_err());
392 }
393
394 #[test]
395 fn flac_extension_without_explicit_codec_should_default_to_flac() {
396 let builder = AudioEncoder::create("output.flac").audio(44100, 2);
397 let mut b = builder;
398 b.apply_container_defaults();
399 assert_eq!(b.audio_codec, AudioCodec::Flac);
400 }
401
402 #[test]
403 fn ogg_extension_without_explicit_codec_should_default_to_vorbis() {
404 let builder = AudioEncoder::create("output.ogg").audio(44100, 2);
405 let mut b = builder;
406 b.apply_container_defaults();
407 assert_eq!(b.audio_codec, AudioCodec::Vorbis);
408 }
409
410 #[test]
411 fn flac_extension_with_explicit_codec_should_not_override() {
412 let builder = AudioEncoder::create("output.flac")
413 .audio(44100, 2)
414 .audio_codec(AudioCodec::Flac);
415 let mut b = builder;
416 b.apply_container_defaults();
417 assert_eq!(b.audio_codec, AudioCodec::Flac);
418 }
419
420 #[test]
421 fn flac_container_enum_without_explicit_codec_should_default_to_flac() {
422 let builder = AudioEncoder::create("output.audio")
423 .audio(44100, 2)
424 .container(OutputContainer::Flac);
425 let mut b = builder;
426 b.apply_container_defaults();
427 assert_eq!(b.audio_codec, AudioCodec::Flac);
428 }
429
430 #[test]
431 fn ogg_container_enum_without_explicit_codec_should_default_to_vorbis() {
432 let builder = AudioEncoder::create("output.audio")
433 .audio(44100, 2)
434 .container(OutputContainer::Ogg);
435 let mut b = builder;
436 b.apply_container_defaults();
437 assert_eq!(b.audio_codec, AudioCodec::Vorbis);
438 }
439
440 #[test]
441 fn flac_extension_with_incompatible_codec_should_return_error() {
442 let result = AudioEncoder::create("output.flac")
443 .audio(44100, 2)
444 .audio_codec(AudioCodec::Mp3)
445 .build();
446 assert!(
447 matches!(
448 result,
449 Err(EncodeError::UnsupportedContainerCodecCombination {
450 ref container,
451 ..
452 }) if container == "flac"
453 ),
454 "expected UnsupportedContainerCodecCombination for flac"
455 );
456 }
457
458 #[test]
459 fn ogg_extension_with_incompatible_codec_should_return_error() {
460 let result = AudioEncoder::create("output.ogg")
461 .audio(44100, 2)
462 .audio_codec(AudioCodec::Mp3)
463 .build();
464 assert!(
465 matches!(
466 result,
467 Err(EncodeError::UnsupportedContainerCodecCombination {
468 ref container,
469 ..
470 }) if container == "ogg"
471 ),
472 "expected UnsupportedContainerCodecCombination for ogg"
473 );
474 }
475
476 #[test]
477 fn ogg_with_opus_should_pass_validation() {
478 let result = AudioEncoder::create("output.ogg")
482 .audio_codec(AudioCodec::Opus)
483 .build();
484 assert!(!matches!(
485 result,
486 Err(EncodeError::UnsupportedContainerCodecCombination { .. })
487 ));
488 }
489
490 #[test]
491 fn non_flac_ogg_extension_should_not_enforce_container_codecs() {
492 let result = AudioEncoder::create("output.mp3")
494 .audio_codec(AudioCodec::Flac)
495 .build();
496 assert!(!matches!(
497 result,
498 Err(EncodeError::UnsupportedContainerCodecCombination { .. })
499 ));
500 }
501}