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, Container, EncodeError};
14
15pub struct AudioEncoderBuilder {
31 pub(crate) path: PathBuf,
32 pub(crate) container: Option<Container>,
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: Container) -> 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 == Container::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 == Container::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 == Container::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 == Container::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 let config = AudioEncoderConfig {
241 path: builder.path.clone(),
242 sample_rate: builder
243 .audio_sample_rate
244 .ok_or_else(|| EncodeError::InvalidConfig {
245 reason: "Audio sample rate not configured".to_string(),
246 })?,
247 channels: builder
248 .audio_channels
249 .ok_or_else(|| EncodeError::InvalidConfig {
250 reason: "Audio channels not configured".to_string(),
251 })?,
252 codec: builder.audio_codec,
253 bitrate: builder.audio_bitrate,
254 codec_options: builder.codec_options,
255 _progress_callback: false,
256 };
257
258 let inner = Some(AudioEncoderInner::new(&config)?);
259
260 Ok(Self {
261 inner,
262 _config: config,
263 _start_time: Instant::now(),
264 })
265 }
266
267 #[must_use]
269 pub fn actual_codec(&self) -> &str {
270 self.inner
271 .as_ref()
272 .map_or("", |inner| inner.actual_codec.as_str())
273 }
274
275 pub fn push(&mut self, frame: &AudioFrame) -> Result<(), EncodeError> {
281 let inner = self
282 .inner
283 .as_mut()
284 .ok_or_else(|| EncodeError::InvalidConfig {
285 reason: "Audio encoder not initialized".to_string(),
286 })?;
287 unsafe { inner.push_frame(frame)? };
289 Ok(())
290 }
291
292 pub fn finish(mut self) -> Result<(), EncodeError> {
298 if let Some(mut inner) = self.inner.take() {
299 unsafe { inner.finish()? };
301 }
302 Ok(())
303 }
304}
305
306impl Drop for AudioEncoder {
307 fn drop(&mut self) {
308 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn create_should_return_builder_without_error() {
318 let _builder: AudioEncoderBuilder = AudioEncoder::create("output.m4a");
319 }
320
321 #[test]
322 fn builder_audio_settings_should_be_stored() {
323 let builder = AudioEncoder::create("output.m4a")
324 .audio(48000, 2)
325 .audio_codec(AudioCodec::Aac)
326 .audio_bitrate(192_000);
327 assert_eq!(builder.audio_sample_rate, Some(48000));
328 assert_eq!(builder.audio_channels, Some(2));
329 assert_eq!(builder.audio_codec, AudioCodec::Aac);
330 assert_eq!(builder.audio_bitrate, Some(192_000));
331 }
332
333 #[test]
334 fn build_without_sample_rate_should_return_error() {
335 let result = AudioEncoder::create("output.m4a").build();
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn flac_extension_without_explicit_codec_should_default_to_flac() {
341 let builder = AudioEncoder::create("output.flac").audio(44100, 2);
342 let mut b = builder;
343 b.apply_container_defaults();
344 assert_eq!(b.audio_codec, AudioCodec::Flac);
345 }
346
347 #[test]
348 fn ogg_extension_without_explicit_codec_should_default_to_vorbis() {
349 let builder = AudioEncoder::create("output.ogg").audio(44100, 2);
350 let mut b = builder;
351 b.apply_container_defaults();
352 assert_eq!(b.audio_codec, AudioCodec::Vorbis);
353 }
354
355 #[test]
356 fn flac_extension_with_explicit_codec_should_not_override() {
357 let builder = AudioEncoder::create("output.flac")
358 .audio(44100, 2)
359 .audio_codec(AudioCodec::Flac);
360 let mut b = builder;
361 b.apply_container_defaults();
362 assert_eq!(b.audio_codec, AudioCodec::Flac);
363 }
364
365 #[test]
366 fn flac_container_enum_without_explicit_codec_should_default_to_flac() {
367 let builder = AudioEncoder::create("output.audio")
368 .audio(44100, 2)
369 .container(Container::Flac);
370 let mut b = builder;
371 b.apply_container_defaults();
372 assert_eq!(b.audio_codec, AudioCodec::Flac);
373 }
374
375 #[test]
376 fn ogg_container_enum_without_explicit_codec_should_default_to_vorbis() {
377 let builder = AudioEncoder::create("output.audio")
378 .audio(44100, 2)
379 .container(Container::Ogg);
380 let mut b = builder;
381 b.apply_container_defaults();
382 assert_eq!(b.audio_codec, AudioCodec::Vorbis);
383 }
384
385 #[test]
386 fn flac_extension_with_incompatible_codec_should_return_error() {
387 let result = AudioEncoder::create("output.flac")
388 .audio(44100, 2)
389 .audio_codec(AudioCodec::Mp3)
390 .build();
391 assert!(
392 matches!(
393 result,
394 Err(EncodeError::UnsupportedContainerCodecCombination {
395 ref container,
396 ..
397 }) if container == "flac"
398 ),
399 "expected UnsupportedContainerCodecCombination for flac"
400 );
401 }
402
403 #[test]
404 fn ogg_extension_with_incompatible_codec_should_return_error() {
405 let result = AudioEncoder::create("output.ogg")
406 .audio(44100, 2)
407 .audio_codec(AudioCodec::Mp3)
408 .build();
409 assert!(
410 matches!(
411 result,
412 Err(EncodeError::UnsupportedContainerCodecCombination {
413 ref container,
414 ..
415 }) if container == "ogg"
416 ),
417 "expected UnsupportedContainerCodecCombination for ogg"
418 );
419 }
420
421 #[test]
422 fn ogg_with_opus_should_pass_validation() {
423 let result = AudioEncoder::create("output.ogg")
427 .audio_codec(AudioCodec::Opus)
428 .build();
429 assert!(!matches!(
430 result,
431 Err(EncodeError::UnsupportedContainerCodecCombination { .. })
432 ));
433 }
434
435 #[test]
436 fn non_flac_ogg_extension_should_not_enforce_container_codecs() {
437 let result = AudioEncoder::create("output.mp3")
439 .audio_codec(AudioCodec::Flac)
440 .build();
441 assert!(!matches!(
442 result,
443 Err(EncodeError::UnsupportedContainerCodecCombination { .. })
444 ));
445 }
446}