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) audio_codec_explicit: bool,
39}
40
41impl AudioEncoderBuilder {
42 pub(crate) fn new(path: PathBuf) -> Self {
43 Self {
44 path,
45 container: None,
46 audio_sample_rate: None,
47 audio_channels: None,
48 audio_codec: AudioCodec::default(),
49 audio_bitrate: None,
50 codec_options: None,
51 audio_codec_explicit: false,
52 }
53 }
54
55 #[must_use]
57 pub fn audio(mut self, sample_rate: u32, channels: u32) -> Self {
58 self.audio_sample_rate = Some(sample_rate);
59 self.audio_channels = Some(channels);
60 self
61 }
62
63 #[must_use]
65 pub fn audio_codec(mut self, codec: AudioCodec) -> Self {
66 self.audio_codec = codec;
67 self.audio_codec_explicit = true;
68 self
69 }
70
71 #[must_use]
73 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
74 self.audio_bitrate = Some(bitrate);
75 self
76 }
77
78 #[must_use]
80 pub fn container(mut self, container: OutputContainer) -> Self {
81 self.container = Some(container);
82 self
83 }
84
85 #[must_use]
90 pub fn codec_options(mut self, opts: AudioCodecOptions) -> Self {
91 self.codec_options = Some(opts);
92 self
93 }
94
95 fn apply_container_defaults(&mut self) {
96 let is_flac = self
97 .path
98 .extension()
99 .and_then(|e| e.to_str())
100 .is_some_and(|e| e.eq_ignore_ascii_case("flac"))
101 || self
102 .container
103 .as_ref()
104 .is_some_and(|c| *c == OutputContainer::Flac);
105 if is_flac && !self.audio_codec_explicit {
106 self.audio_codec = AudioCodec::Flac;
107 }
108
109 let is_ogg = self
110 .path
111 .extension()
112 .and_then(|e| e.to_str())
113 .is_some_and(|e| e.eq_ignore_ascii_case("ogg"))
114 || self
115 .container
116 .as_ref()
117 .is_some_and(|c| *c == OutputContainer::Ogg);
118 if is_ogg && !self.audio_codec_explicit {
119 self.audio_codec = AudioCodec::Vorbis;
120 }
121 }
122
123 pub fn build(self) -> Result<AudioEncoder, EncodeError> {
130 AudioEncoder::from_builder(self)
131 }
132}
133
134pub struct AudioEncoder {
150 inner: Option<AudioEncoderInner>,
151 _config: AudioEncoderConfig,
152 _start_time: Instant,
153}
154
155impl AudioEncoder {
156 pub fn create<P: AsRef<std::path::Path>>(path: P) -> AudioEncoderBuilder {
161 AudioEncoderBuilder::new(path.as_ref().to_path_buf())
162 }
163
164 pub(crate) fn from_builder(mut builder: AudioEncoderBuilder) -> Result<Self, EncodeError> {
165 builder.apply_container_defaults();
166
167 let is_flac = builder
169 .path
170 .extension()
171 .and_then(|e| e.to_str())
172 .is_some_and(|e| e.eq_ignore_ascii_case("flac"))
173 || builder
174 .container
175 .as_ref()
176 .is_some_and(|c| *c == OutputContainer::Flac);
177 if is_flac && !matches!(builder.audio_codec, AudioCodec::Flac) {
178 return Err(EncodeError::UnsupportedContainerCodecCombination {
179 container: "flac".to_string(),
180 codec: builder.audio_codec.name().to_string(),
181 hint: "FLAC container only supports the FLAC codec".to_string(),
182 });
183 }
184
185 let is_ogg = builder
187 .path
188 .extension()
189 .and_then(|e| e.to_str())
190 .is_some_and(|e| e.eq_ignore_ascii_case("ogg"))
191 || builder
192 .container
193 .as_ref()
194 .is_some_and(|c| *c == OutputContainer::Ogg);
195 if is_ogg && !matches!(builder.audio_codec, AudioCodec::Vorbis | AudioCodec::Opus) {
196 return Err(EncodeError::UnsupportedContainerCodecCombination {
197 container: "ogg".to_string(),
198 codec: builder.audio_codec.name().to_string(),
199 hint: "OGG container supports Vorbis and Opus".to_string(),
200 });
201 }
202
203 if let Some(AudioCodecOptions::Opus(ref opts)) = builder.codec_options
205 && let Some(dur) = opts.frame_duration_ms
206 && ![2u32, 5, 10, 20, 40, 60].contains(&dur)
207 {
208 return Err(EncodeError::InvalidOption {
209 name: "frame_duration_ms".to_string(),
210 reason: "must be one of: 2, 5, 10, 20, 40, 60".to_string(),
211 });
212 }
213 if let Some(AudioCodecOptions::Aac(ref opts)) = builder.codec_options
214 && let Some(q) = opts.vbr_quality
215 && !(1..=5).contains(&q)
216 {
217 return Err(EncodeError::InvalidOption {
218 name: "vbr_quality".to_string(),
219 reason: "must be 1–5".to_string(),
220 });
221 }
222 if let Some(AudioCodecOptions::Mp3(ref opts)) = builder.codec_options
223 && let Mp3Quality::Vbr(q) = opts.quality
224 && q > 9
225 {
226 return Err(EncodeError::InvalidOption {
227 name: "vbr_quality".to_string(),
228 reason: "must be 0–9 (0=best)".to_string(),
229 });
230 }
231 if let Some(AudioCodecOptions::Flac(ref opts)) = builder.codec_options
232 && opts.compression_level > 12
233 {
234 return Err(EncodeError::InvalidOption {
235 name: "compression_level".to_string(),
236 reason: "must be 0–12".to_string(),
237 });
238 }
239
240 if let Some(ch) = builder.audio_channels
242 && ch > 8
243 {
244 log::warn!("audio channel count out of range count={ch} maximum=8");
245 return Err(EncodeError::InvalidChannelCount { count: ch });
246 }
247 if let Some(sr) = builder.audio_sample_rate
248 && !(8_000..=384_000).contains(&sr)
249 {
250 log::warn!("audio sample rate out of range rate={sr} minimum=8000 maximum=384000");
251 return Err(EncodeError::InvalidSampleRate { rate: sr });
252 }
253
254 let config = AudioEncoderConfig {
255 path: builder.path.clone(),
256 sample_rate: builder
257 .audio_sample_rate
258 .ok_or_else(|| EncodeError::InvalidConfig {
259 reason: "Audio sample rate not configured".to_string(),
260 })?,
261 channels: builder
262 .audio_channels
263 .ok_or_else(|| EncodeError::InvalidConfig {
264 reason: "Audio channels not configured".to_string(),
265 })?,
266 codec: builder.audio_codec,
267 bitrate: builder.audio_bitrate,
268 codec_options: builder.codec_options,
269 _progress_callback: false,
270 };
271
272 let inner = Some(AudioEncoderInner::new(&config)?);
273
274 Ok(Self {
275 inner,
276 _config: config,
277 _start_time: Instant::now(),
278 })
279 }
280
281 #[must_use]
283 pub fn actual_codec(&self) -> &str {
284 self.inner
285 .as_ref()
286 .map_or("", |inner| inner.actual_codec.as_str())
287 }
288
289 pub fn push(&mut self, frame: &AudioFrame) -> Result<(), EncodeError> {
295 let inner = self
296 .inner
297 .as_mut()
298 .ok_or_else(|| EncodeError::InvalidConfig {
299 reason: "Audio encoder not initialized".to_string(),
300 })?;
301 inner.push_frame(frame)?;
302 Ok(())
303 }
304
305 pub fn finish(mut self) -> Result<(), EncodeError> {
311 if let Some(mut inner) = self.inner.take() {
312 inner.finish()?;
313 }
314 Ok(())
315 }
316}
317
318impl Drop for AudioEncoder {
319 fn drop(&mut self) {
320 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn create_should_return_builder_without_error() {
330 let _builder: AudioEncoderBuilder = AudioEncoder::create("output.m4a");
331 }
332
333 #[test]
334 fn builder_audio_settings_should_be_stored() {
335 let builder = AudioEncoder::create("output.m4a")
336 .audio(48000, 2)
337 .audio_codec(AudioCodec::Aac)
338 .audio_bitrate(192_000);
339 assert_eq!(builder.audio_sample_rate, Some(48000));
340 assert_eq!(builder.audio_channels, Some(2));
341 assert_eq!(builder.audio_codec, AudioCodec::Aac);
342 assert_eq!(builder.audio_bitrate, Some(192_000));
343 }
344
345 #[test]
346 fn build_without_sample_rate_should_return_error() {
347 let result = AudioEncoder::create("output.m4a").build();
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn flac_extension_without_explicit_codec_should_default_to_flac() {
353 let builder = AudioEncoder::create("output.flac").audio(44100, 2);
354 let mut b = builder;
355 b.apply_container_defaults();
356 assert_eq!(b.audio_codec, AudioCodec::Flac);
357 }
358
359 #[test]
360 fn ogg_extension_without_explicit_codec_should_default_to_vorbis() {
361 let builder = AudioEncoder::create("output.ogg").audio(44100, 2);
362 let mut b = builder;
363 b.apply_container_defaults();
364 assert_eq!(b.audio_codec, AudioCodec::Vorbis);
365 }
366
367 #[test]
368 fn flac_extension_with_explicit_codec_should_not_override() {
369 let builder = AudioEncoder::create("output.flac")
370 .audio(44100, 2)
371 .audio_codec(AudioCodec::Flac);
372 let mut b = builder;
373 b.apply_container_defaults();
374 assert_eq!(b.audio_codec, AudioCodec::Flac);
375 }
376
377 #[test]
378 fn flac_container_enum_without_explicit_codec_should_default_to_flac() {
379 let builder = AudioEncoder::create("output.audio")
380 .audio(44100, 2)
381 .container(OutputContainer::Flac);
382 let mut b = builder;
383 b.apply_container_defaults();
384 assert_eq!(b.audio_codec, AudioCodec::Flac);
385 }
386
387 #[test]
388 fn ogg_container_enum_without_explicit_codec_should_default_to_vorbis() {
389 let builder = AudioEncoder::create("output.audio")
390 .audio(44100, 2)
391 .container(OutputContainer::Ogg);
392 let mut b = builder;
393 b.apply_container_defaults();
394 assert_eq!(b.audio_codec, AudioCodec::Vorbis);
395 }
396
397 #[test]
398 fn flac_extension_with_incompatible_codec_should_return_error() {
399 let result = AudioEncoder::create("output.flac")
400 .audio(44100, 2)
401 .audio_codec(AudioCodec::Mp3)
402 .build();
403 assert!(
404 matches!(
405 result,
406 Err(EncodeError::UnsupportedContainerCodecCombination {
407 ref container,
408 ..
409 }) if container == "flac"
410 ),
411 "expected UnsupportedContainerCodecCombination for flac"
412 );
413 }
414
415 #[test]
416 fn ogg_extension_with_incompatible_codec_should_return_error() {
417 let result = AudioEncoder::create("output.ogg")
418 .audio(44100, 2)
419 .audio_codec(AudioCodec::Mp3)
420 .build();
421 assert!(
422 matches!(
423 result,
424 Err(EncodeError::UnsupportedContainerCodecCombination {
425 ref container,
426 ..
427 }) if container == "ogg"
428 ),
429 "expected UnsupportedContainerCodecCombination for ogg"
430 );
431 }
432
433 #[test]
434 fn ogg_with_opus_should_pass_validation() {
435 let result = AudioEncoder::create("output.ogg")
439 .audio_codec(AudioCodec::Opus)
440 .build();
441 assert!(!matches!(
442 result,
443 Err(EncodeError::UnsupportedContainerCodecCombination { .. })
444 ));
445 }
446
447 #[test]
448 fn non_flac_ogg_extension_should_not_enforce_container_codecs() {
449 let result = AudioEncoder::create("output.mp3")
451 .audio_codec(AudioCodec::Flac)
452 .build();
453 assert!(!matches!(
454 result,
455 Err(EncodeError::UnsupportedContainerCodecCombination { .. })
456 ));
457 }
458}