Skip to main content

ff_encode/audio/
builder.rs

1//! Audio encoder builder and public API.
2//!
3//! This module provides [`AudioEncoderBuilder`] for fluent configuration and
4//! [`AudioEncoder`] for encoding audio frames to a file.
5
6use std::path::PathBuf;
7use std::time::Instant;
8
9use ff_format::AudioFrame;
10
11use super::encoder_inner::{AudioEncoderConfig, AudioEncoderInner};
12use crate::{AudioCodec, Container, EncodeError};
13
14/// Builder for constructing an [`AudioEncoder`].
15///
16/// Created by calling [`AudioEncoder::create()`]. Call [`build()`](Self::build)
17/// to open the output file and prepare for encoding.
18///
19/// # Examples
20///
21/// ```ignore
22/// use ff_encode::{AudioEncoder, AudioCodec};
23///
24/// let mut encoder = AudioEncoder::create("output.m4a")
25///     .audio(48000, 2)
26///     .audio_codec(AudioCodec::Aac)
27///     .build()?;
28/// ```
29pub struct AudioEncoderBuilder {
30    pub(crate) path: PathBuf,
31    pub(crate) container: Option<Container>,
32    pub(crate) audio_sample_rate: Option<u32>,
33    pub(crate) audio_channels: Option<u32>,
34    pub(crate) audio_codec: AudioCodec,
35    pub(crate) audio_bitrate: Option<u64>,
36}
37
38impl AudioEncoderBuilder {
39    pub(crate) fn new(path: PathBuf) -> Self {
40        Self {
41            path,
42            container: None,
43            audio_sample_rate: None,
44            audio_channels: None,
45            audio_codec: AudioCodec::default(),
46            audio_bitrate: None,
47        }
48    }
49
50    /// Configure audio stream settings.
51    #[must_use]
52    pub fn audio(mut self, sample_rate: u32, channels: u32) -> Self {
53        self.audio_sample_rate = Some(sample_rate);
54        self.audio_channels = Some(channels);
55        self
56    }
57
58    /// Set audio codec.
59    #[must_use]
60    pub fn audio_codec(mut self, codec: AudioCodec) -> Self {
61        self.audio_codec = codec;
62        self
63    }
64
65    /// Set audio bitrate in bits per second.
66    #[must_use]
67    pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
68        self.audio_bitrate = Some(bitrate);
69        self
70    }
71
72    /// Set container format explicitly (usually auto-detected from file extension).
73    #[must_use]
74    pub fn container(mut self, container: Container) -> Self {
75        self.container = Some(container);
76        self
77    }
78
79    /// Validate builder state and open the output file.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`EncodeError`] if configuration is invalid, the output path
84    /// cannot be created, or no suitable encoder is found.
85    pub fn build(self) -> Result<AudioEncoder, EncodeError> {
86        AudioEncoder::from_builder(self)
87    }
88}
89
90/// Encodes audio frames to a file using FFmpeg.
91///
92/// # Construction
93///
94/// Use [`AudioEncoder::create()`] to get an [`AudioEncoderBuilder`], then call
95/// [`AudioEncoderBuilder::build()`]:
96///
97/// ```ignore
98/// use ff_encode::{AudioEncoder, AudioCodec};
99///
100/// let mut encoder = AudioEncoder::create("output.m4a")
101///     .audio(48000, 2)
102///     .audio_codec(AudioCodec::Aac)
103///     .build()?;
104/// ```
105pub struct AudioEncoder {
106    inner: Option<AudioEncoderInner>,
107    _config: AudioEncoderConfig,
108    _start_time: Instant,
109}
110
111impl AudioEncoder {
112    /// Creates a builder for the specified output file path.
113    ///
114    /// This method is infallible. Validation occurs when
115    /// [`AudioEncoderBuilder::build()`] is called.
116    pub fn create<P: AsRef<std::path::Path>>(path: P) -> AudioEncoderBuilder {
117        AudioEncoderBuilder::new(path.as_ref().to_path_buf())
118    }
119
120    pub(crate) fn from_builder(builder: AudioEncoderBuilder) -> Result<Self, EncodeError> {
121        let config = AudioEncoderConfig {
122            path: builder.path.clone(),
123            sample_rate: builder
124                .audio_sample_rate
125                .ok_or_else(|| EncodeError::InvalidConfig {
126                    reason: "Audio sample rate not configured".to_string(),
127                })?,
128            channels: builder
129                .audio_channels
130                .ok_or_else(|| EncodeError::InvalidConfig {
131                    reason: "Audio channels not configured".to_string(),
132                })?,
133            codec: builder.audio_codec,
134            bitrate: builder.audio_bitrate,
135            _progress_callback: false,
136        };
137
138        let inner = Some(AudioEncoderInner::new(&config)?);
139
140        Ok(Self {
141            inner,
142            _config: config,
143            _start_time: Instant::now(),
144        })
145    }
146
147    /// Returns the name of the FFmpeg encoder actually used (e.g. `"aac"`, `"libopus"`).
148    #[must_use]
149    pub fn actual_codec(&self) -> &str {
150        self.inner
151            .as_ref()
152            .map_or("", |inner| inner.actual_codec.as_str())
153    }
154
155    /// Pushes an audio frame for encoding.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`EncodeError`] if encoding fails or the encoder is not initialised.
160    pub fn push(&mut self, frame: &AudioFrame) -> Result<(), EncodeError> {
161        let inner = self
162            .inner
163            .as_mut()
164            .ok_or_else(|| EncodeError::InvalidConfig {
165                reason: "Audio encoder not initialized".to_string(),
166            })?;
167        // SAFETY: inner is properly initialised and we have exclusive access.
168        unsafe { inner.push_frame(frame)? };
169        Ok(())
170    }
171
172    /// Flushes remaining frames and writes the file trailer.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`EncodeError`] if finalising fails.
177    pub fn finish(mut self) -> Result<(), EncodeError> {
178        if let Some(mut inner) = self.inner.take() {
179            // SAFETY: inner is properly initialised and we have exclusive access.
180            unsafe { inner.finish()? };
181        }
182        Ok(())
183    }
184}
185
186impl Drop for AudioEncoder {
187    fn drop(&mut self) {
188        // AudioEncoderInner handles cleanup in its own Drop.
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn create_should_return_builder_without_error() {
198        let _builder: AudioEncoderBuilder = AudioEncoder::create("output.m4a");
199    }
200
201    #[test]
202    fn builder_audio_settings_should_be_stored() {
203        let builder = AudioEncoder::create("output.m4a")
204            .audio(48000, 2)
205            .audio_codec(AudioCodec::Aac)
206            .audio_bitrate(192_000);
207        assert_eq!(builder.audio_sample_rate, Some(48000));
208        assert_eq!(builder.audio_channels, Some(2));
209        assert_eq!(builder.audio_codec, AudioCodec::Aac);
210        assert_eq!(builder.audio_bitrate, Some(192_000));
211    }
212
213    #[test]
214    fn build_without_sample_rate_should_return_error() {
215        let result = AudioEncoder::create("output.m4a").build();
216        assert!(result.is_err());
217    }
218}